├── .github
├── format.sh
└── workflows
│ ├── code_formatting.yml
│ └── unit_testing.yml
├── .gitignore
├── LICENSE
├── README.md
├── assets
├── shunpo_demo.gif
├── shunpo_logo.png
└── shunpo_logo_inverted.png
├── install.sh
├── nix
├── NixREADME.md
└── flake.nix
├── src
├── add_bookmark.sh
├── clear_bookmarks.sh
├── colors.sh
├── functions.sh
├── go_to_bookmark.sh
├── jump_to_child.sh
├── jump_to_parent.sh
├── list_bookmarks.sh
└── remove_bookmark.sh
├── tests
├── bats
│ ├── Notes.md
│ ├── assert.sh
│ ├── error.sh
│ ├── lang.sh
│ └── output.sh
├── common.sh
├── test_bookmarks.bats
└── test_navigation.bats
└── uninstall.sh
/.github/format.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4 | shfmt -w -i 4 -ci -s -l "$SCRIPT_DIR/.."
5 |
--------------------------------------------------------------------------------
/.github/workflows/code_formatting.yml:
--------------------------------------------------------------------------------
1 | name: Code Formatting
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | format-check:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout.
17 | uses: actions/checkout@v4
18 |
19 | - name: Install shfmt.
20 | run: sudo apt-get update && sudo apt-get install -y shfmt
21 |
22 | - name: Check Formatting.
23 | run: |
24 | shfmt -d -i 4 -ci -s . > shfmt-diff.txt
25 | if [[ -s shfmt-diff.txt ]]; then
26 | echo "Code is not Formatted Correctly:"
27 | cat shfmt-diff.txt
28 | exit 1
29 | fi
30 |
31 | - name: Success.
32 | if: success()
33 | run: echo "Code Formatting Passed."
34 |
35 | - name: Failure.
36 | if: failure()
37 | run: echo "Code Formatting Failed."
38 |
--------------------------------------------------------------------------------
/.github/workflows/unit_testing.yml:
--------------------------------------------------------------------------------
1 | name: Unit Testing
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | unit-testing:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout.
17 | uses: actions/checkout@v4
18 |
19 | - name: Set TERM environment variable
20 | run: echo "TERM=xterm-256color" >> $GITHUB_ENV
21 |
22 | - name: Install BATS.
23 | run: sudo apt update && sudo apt install -y bats
24 |
25 | - name: Run Unit Tests.
26 | run: script -q -c "bats tests/"
27 |
28 | - name: Success.
29 | if: success()
30 | run: echo "Unit Testing Passed."
31 |
32 | - name: Failure.
33 | if: failure()
34 | run: echo "Unit Testing Failed."
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | flake.lock
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Raphael Eguchi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Quick navigation with minimal mental overhead.
7 |
8 |
9 | ----
10 | Shunpo is a minimalist bash tool that tries to make directory navigation in terminal just a little bit faster by providing a simple system to manage bookmarks and jump to directories with only a few keystrokes.
11 | If you frequently need to use commands like `cd`, `pushd`, or `popd`, Shunpo is for you.
12 |
13 | 
14 | [](https://ko-fi.com/egurapha)
15 | 
16 | 
17 | 
18 |
19 | Requirements
20 | ----
21 | Bash 3.2 or newer.
22 |
23 | Installation
24 | ----
25 | Run `install.sh && source ~/.bashrc`. For nix, click [here](nix/NixREADME.md).
26 |
27 | Tutorial
28 | ----
29 | Click [here](https://www.youtube.com/watch?v=TN66A3MPo50) for a video tutorial.
30 |
31 |
32 | Commands
33 | ----
34 | #### Bookmarking:
35 | `sb`: Add the current directory to bookmarks.
36 | `sg`, `sg [#]` : Go to a bookmark.
37 | `sr`, `sr [#]` : Remove a bookmark.
38 | `sl`: List all bookmarks.
39 | `sc`: Clear all bookmarks.
40 |
41 | #### Navigation:
42 | `sj`, `sj [#]`: "Jump" up to a parent directory.
43 | `sd`: "Dive" down to a child directory.
44 |
45 | #### Selection:
46 | `0~9`: Select an option.
47 | `n`: Next page.
48 | `p`: Previous page.
49 | `b`: Move selection back to parent directory. (For `sd` only.)
50 | `Enter`: Navigate to selected directory (For `sd` only.)
51 |
52 | Uninstalling
53 | ----
54 | Run `uninstall.sh`
55 |
56 |
--------------------------------------------------------------------------------
/assets/shunpo_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/egurapha/Shunpo/5fd3883801ea83cabb21b506cd343a9420b3bd35/assets/shunpo_demo.gif
--------------------------------------------------------------------------------
/assets/shunpo_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/egurapha/Shunpo/5fd3883801ea83cabb21b506cd343a9420b3bd35/assets/shunpo_logo.png
--------------------------------------------------------------------------------
/assets/shunpo_logo_inverted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/egurapha/Shunpo/5fd3883801ea83cabb21b506cd343a9420b3bd35/assets/shunpo_logo_inverted.png
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Get install paths.
4 | DEFAULT_INSTALL_DIR=${XDG_DATA_HOME:-$HOME/.local/share}/shunpo
5 | read -p "Enter the installation directory [default: $DEFAULT_INSTALL_DIR]: " user_input
6 | INSTALL_DIR=${user_input:-"$DEFAULT_INSTALL_DIR"}
7 | SCRIPT_DIR=${INSTALL_DIR}/scripts/
8 | BASHRC="$HOME/.bashrc"
9 |
10 | # File containing command definitions.
11 | SHUNPO_CMD="$INSTALL_DIR/shunpo_cmd"
12 |
13 | setup() {
14 | mkdir -p $INSTALL_DIR
15 | mkdir -p $SCRIPT_DIR
16 | if [ -f $SHUNPO_CMD ]; then
17 | rm $SHUNPO_CMD
18 | fi
19 | touch $SHUNPO_CMD
20 | }
21 |
22 | add_commands() {
23 | # Define command set.
24 | SCRIPT_DIR="$(realpath "$SCRIPT_DIR")"
25 | cat >"$SHUNPO_CMD" <"$temp_file"
45 | mv "$temp_file" "$BASHRC"
46 | echo "$source_rc_line" >>"$BASHRC"
47 | echo "Added to BASHRC: $source_rc_line"
48 |
49 | # Record SHUNPO_DIR for uninstallation (overwrite).
50 | install_dir_line="export SHUNPO_DIR=$INSTALL_DIR" >>"$BASHRC$"
51 | temp_file=$(mktemp)
52 | grep -v '^export SHUNPO_DIR=' "$BASHRC" >"$temp_file"
53 | mv "$temp_file" "$BASHRC"
54 | echo "$install_dir_line" >>"$BASHRC"
55 | echo "Added to BASHRC: $install_dir_line"
56 |
57 | add_commands
58 | }
59 |
60 | # Install.
61 | echo "Installing."
62 | setup
63 | install
64 |
65 | echo "Done."
66 | echo "(Remember to run source ~/.bashrc.)"
67 |
--------------------------------------------------------------------------------
/nix/NixREADME.md:
--------------------------------------------------------------------------------
1 | # Shunpo with Nix ❄️
2 | A nix flake for Shunpo is provided at `nix/flake.nix`. The nix flake can either be built for testing, or fully installed.
3 |
4 | Building
5 | ---
6 | 1. Within the `nix/` directory, run `nix build .` which will generate a folder called `result/`.
7 | 2. Intialize Shunpo by running `source result/bin/shunpo_init`.
8 |
9 | Installation
10 | ---
11 | To install Shunpo using the nix flake,
12 | 1. Run `nix profile install .#shunpo` within the `nix/` directory.
13 | 2. Start a new terminal session and initialize Shunpo by running `source shunpo_init`.
14 | 3. If you would like to have Shunpo initialize automatically, run the command below, which will append a line to your `.bashrc` to `source` the initialization file.
15 | ```
16 | echo "source shunpo_init" >> ~/.bashrc
17 | ```
18 |
--------------------------------------------------------------------------------
/nix/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Shunpo: A minimalist bash tool for quick directory navigation";
3 | inputs = {
4 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
5 | };
6 |
7 | outputs = { self, nixpkgs }:
8 | let
9 | supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
10 | forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system:
11 | let pkgs = import nixpkgs { inherit system; };
12 | in f pkgs
13 | );
14 | in
15 | {
16 | packages = forAllSystems
17 | (pkgs: {
18 | default = self.packages.${pkgs.system}.shunpo;
19 | shunpo = pkgs.stdenv.mkDerivation {
20 | pname = "shunpo";
21 | version = "1.0.3";
22 |
23 | src = builtins.path { path = ../src; };
24 | buildInputs = [ pkgs.bash pkgs.shfmt ];
25 |
26 | installPhase = ''
27 | # Generate installation directories and files.
28 | INSTALL_DIR=$out/bin
29 | SCRIPT_DIR=$out/scripts
30 | mkdir -p $INSTALL_DIR
31 | mkdir -p $SCRIPT_DIR
32 |
33 | # Define commands.
34 | SHUNPO_CMD=$SCRIPT_DIR/shunpo_cmd
35 | touch $SHUNPO_CMD
36 |
37 | cat > "$SHUNPO_CMD" < "$SHUNPO_INIT"
54 | chmod +x $SHUNPO_INIT # not necessary, but keep for auto-complete.
55 | '';
56 |
57 |
58 | meta = {
59 | description = "Shunpo: A minimalist bash tool for quick directory navigation";
60 | license = nixpkgs.lib.licenses.mit;
61 | maintainers = [ "egurapha" ];
62 | platforms = supportedSystems;
63 | };
64 | };
65 | });
66 |
67 | devShells = forAllSystems (pkgs: {
68 | shunpo = pkgs.mkShell {
69 | buildInputs = [ pkgs.bash ];
70 | };
71 | });
72 | };
73 | }
74 |
--------------------------------------------------------------------------------
/src/add_bookmark.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Imports.
4 | SHUNPO_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5 | source "$SHUNPO_SCRIPT_DIR"/colors.sh
6 | source "$SHUNPO_SCRIPT_DIR"/functions.sh
7 |
8 | # File to store bookmarks.
9 | SHUNPO_MAX_BOOKMARKS=128
10 |
11 | # Ensure the bookmarks file exists and is not empty
12 | if [ ! -f "$SHUNPO_BOOKMARKS_FILE" ]; then
13 | touch "$SHUNPO_BOOKMARKS_FILE"
14 | fi
15 |
16 | # Check the number of existing bookmarks.
17 | current_bookmarks=$(wc -l <"$SHUNPO_BOOKMARKS_FILE")
18 |
19 | # If the bookmarks list is full, print a message and do not add the new bookmark.
20 | if [ "$current_bookmarks" -ge "$SHUNPO_MAX_BOOKMARKS" ]; then
21 | echo -e "${SHUNPO_CYAN}${SHUNPO_BOLD}Bookmarks list is full!${SHUNPO_RESET} Maximum of $SHUNPO_MAX_BOOKMARKS bookmarks allowed."
22 | exit 1
23 | fi
24 |
25 | # Save the current directory to the bookmarks file.
26 | current_dir=$(realpath "$PWD")
27 | if ! grep -q -x "$(printf '%s\n' "$current_dir")" "$SHUNPO_BOOKMARKS_FILE"; then
28 | echo "$current_dir" >>"$SHUNPO_BOOKMARKS_FILE"
29 | echo -e "${SHUNPO_GREEN}${SHUNPO_BOLD}Bookmark added:${SHUNPO_RESET} $current_dir"
30 | else
31 | echo -e "${SHUNPO_ORANGE}${SHUNPO_BOLD}Bookmark exists:${SHUNPO_RESET} $current_dir"
32 | fi
33 |
--------------------------------------------------------------------------------
/src/clear_bookmarks.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Colors and formatting.
4 | SHUNPO_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5 | source "$SHUNPO_SCRIPT_DIR"/colors.sh
6 | source "$SHUNPO_SCRIPT_DIR"/functions.sh
7 |
8 | # Remove the bookmarks file if it exists.
9 | if [ ! -f "$SHUNPO_BOOKMARKS_FILE" ] || [ ! -s "$SHUNPO_BOOKMARKS_FILE" ]; then
10 | echo -e "${SHUNPO_ORANGE}${SHUNPO_BOLD}No Bookmarks Found.${SHUNPO_RESET}"
11 | exit 1
12 |
13 | else
14 | rm "$SHUNPO_BOOKMARKS_FILE"
15 | echo -e "${SHUNPO_RED}${SHUNPO_BOLD}Cleared Bookmarks.${SHUNPO_RESET}"
16 | fi
17 |
--------------------------------------------------------------------------------
/src/colors.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | SHUNPO_CYAN='\033[36m'
3 | SHUNPO_ORANGE='\033[38;5;208m'
4 | SHUNPO_GREEN='\033[38;5;43m'
5 | SHUNPO_RED='\033[38;5;203m'
6 | SHUNPO_BOLD='\033[1m'
7 | SHUNPO_RESET='\033[0m'
8 |
--------------------------------------------------------------------------------
/src/functions.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Default Bookmarks Path.
4 | SHUNPO_BOOKMARKS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/shunpo/"
5 | if [ ! -d "$SHUNPO_BOOKMARKS_DIR" ]; then
6 | mkdir -p "$SHUNPO_BOOKMARKS_DIR"
7 | fi
8 |
9 | SHUNPO_BOOKMARKS_FILE="$SHUNPO_BOOKMARKS_DIR/.shunpo_bookmarks"
10 |
11 | # Function to display bookmarks with pagination.
12 | function shunpo_interact_bookmarks() {
13 | local bookmarks=()
14 | local total_bookmarks
15 | local current_page=0
16 | local max_per_page=10
17 | local last_page
18 | local start_index
19 | local end_index
20 | local padding_lines
21 |
22 | tput civis
23 |
24 | # Read bookmarks into an array.
25 | while IFS= read -r bookmark; do
26 | bookmarks+=("$bookmark")
27 | done <"$SHUNPO_BOOKMARKS_FILE"
28 |
29 | # Check that bookmarks file is not empty.
30 | total_bookmarks=${#bookmarks[@]}
31 | if [ "$total_bookmarks" -eq 0 ]; then
32 | echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}No Bookmarks Found.${SHUNPO_RESET}"
33 | return 0
34 | fi
35 |
36 | # If a selection is specified, select it.
37 | if [ -n "$2" ]; then
38 | if ! [[ $2 =~ ^[0-9]+$ ]]; then
39 | echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}Invalid Bookmark Selection.${SHUNPO_RESET}"
40 | return 1
41 | else
42 | if [[ $2 -lt $total_bookmarks ]]; then
43 | shunpo_selected_dir="${bookmarks[$2]}"
44 | tput cnorm
45 | return 0
46 | else
47 | tput cnorm
48 | return 2
49 | fi
50 | fi
51 | fi
52 |
53 | last_page=$(((total_bookmarks + max_per_page - 1) / max_per_page))
54 |
55 | # Pagination loop.
56 | while true; do
57 | # Calculate the start and end indices for the current page.
58 | start_index=$((current_page * max_per_page))
59 | end_index=$((start_index + max_per_page))
60 | if [ "$end_index" -gt "$total_bookmarks" ]; then
61 | end_index=$total_bookmarks
62 | fi
63 |
64 | # Pad the bottom of the terminal to avoid erroneous printing.
65 | if [ "$max_per_page" -lt "$total_bookmarks" ]; then
66 | padding_lines=$max_per_page
67 | else
68 | padding_lines=$total_bookmarks
69 | fi
70 |
71 | if [ $last_page -gt 1 ]; then
72 | padding_lines=$((padding_lines + 1)) # page numbers.
73 | fi
74 |
75 | padding_lines=$((padding_lines + 1)) # header.
76 | shunpo_add_space $padding_lines
77 |
78 | tput sc
79 | echo -e "${SHUNPO_BOLD}${SHUNPO_CYAN}Shunpo <$1>${SHUNPO_RESET}"
80 |
81 | # Display bookmarks for the current page.
82 | for ((i = start_index; i < end_index; i++)); do
83 | echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}$((i - start_index))${SHUNPO_RESET}] ${bookmarks[i]}"
84 | done
85 |
86 | if [ $last_page -gt 1 ]; then
87 | echo -e "${SHUNPO_CYAN}[$((current_page + 1)) / $((last_page))]${SHUNPO_RESET}"
88 | fi
89 |
90 | # Read input to select bookmarks and cycle through pages.
91 | read -rsn1 input
92 | if [[ $input == "n" ]]; then
93 | if [ $((current_page + 1)) -le $((last_page - 1)) ]; then
94 | current_page=$((current_page + 1))
95 | fi
96 | shunpo_clear_output
97 |
98 | elif [[ $input == "p" ]]; then
99 | if [ $((current_page - 1)) -ge 0 ]; then
100 | current_page=$((current_page - 1))
101 | fi
102 | shunpo_clear_output
103 |
104 | elif [[ $input =~ ^[0-9]+$ ]] && [ "$input" -ge 0 ] && [ "$input" -lt $max_per_page ]; then
105 | # Process bookmark selection input.
106 | shunpo_selected_bookmark_index=$((current_page * max_per_page + input))
107 | if [[ $shunpo_selected_bookmark_index -lt $total_bookmarks ]]; then
108 | shunpo_selected_dir="${bookmarks[$shunpo_selected_bookmark_index]}"
109 | shunpo_clear_output
110 | tput cnorm
111 | return 0
112 | else
113 | shunpo_clear_output
114 | fi
115 | else
116 | shunpo_clear_output
117 | tput cnorm
118 | return 0
119 | fi
120 | done
121 | shunpo_clear_output
122 | tput cnorm
123 | return 0
124 | }
125 |
126 | function shunpo_jump_to_parent_dir() {
127 | local current_dir=$(pwd)
128 | local max_levels=128
129 | local parent_dirs=()
130 | local total_parents
131 | local current_page=0
132 | local max_per_page=10
133 | local last_page
134 | local start_index
135 | local end_index
136 | local padding_lines
137 |
138 | tput civis
139 |
140 | # Collect all parent directories.
141 | while [ "$current_dir" != "/" ] && [ "${#parent_dirs[@]}" -lt "$max_levels" ]; do
142 | parent_dirs+=("$current_dir")
143 | current_dir=$(dirname "$current_dir")
144 | done
145 |
146 | parent_dirs+=("/") # Add root.
147 |
148 | # Check if we are at the root.
149 | if [[ ${#parent_dirs[@]} -eq 1 && ${parent_dirs[0]} == "/" ]]; then
150 | echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}No Parent Directories.${SHUNPO_RESET}"
151 | return 1
152 | fi
153 |
154 | total_parents=${#parent_dirs[@]}
155 |
156 | # If a selection is specified, select it.
157 | if [ -n "$1" ]; then
158 | if ! [[ $1 =~ ^[0-9]+$ ]]; then
159 | echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}Invalid Parent Selection.${SHUNPO_RESET}"
160 | return 1
161 | else
162 | if [[ $1 -lt $total_parents ]]; then
163 | cd "${parent_dirs[$1]}" || exit
164 | tput cnorm
165 | echo -e "${SHUNPO_GREEN}${SHUNPO_BOLD}Changed to:${SHUNPO_RESET} ${parent_dirs[$1]}"
166 | return 0
167 | else
168 | tput cnorm
169 | return 2
170 | fi
171 | fi
172 | fi
173 |
174 | last_page=$(((total_parents + max_per_page - 1) / max_per_page))
175 |
176 | while true; do
177 | # Calculate start and end indices for pagination.
178 | start_index=$((current_page * max_per_page))
179 | end_index=$((start_index + max_per_page))
180 | if [ "$end_index" -gt "$total_parents" ]; then
181 | end_index=$total_parents
182 | fi
183 |
184 | # Pad the bottom of the terminal to avoid erroneous printing.
185 | if [ "$max_per_page" -lt "$total_parents" ]; then
186 | padding_lines=$max_per_page
187 | else
188 | padding_lines=$total_parents
189 | fi
190 |
191 | if [ $last_page -gt 1 ]; then
192 | padding_lines=$((padding_lines + 1)) # page numbers.
193 | fi
194 |
195 | padding_lines=$((padding_lines + 1)) # header.
196 | shunpo_add_space $padding_lines
197 |
198 | tput sc
199 | echo -e "${SHUNPO_BOLD}${SHUNPO_CYAN}Shunpo ${SHUNPO_RESET}"
200 |
201 | # Display the current page of parent directories.
202 | for ((i = start_index; i < end_index; i++)); do
203 | if [[ $i -eq $((end_index - 1)) && $current_page -eq $((last_page - 1)) ]]; then
204 | echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}$((i - start_index))${SHUNPO_RESET}] $(basename ${parent_dirs[i]})"
205 | else
206 | echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}$((i - start_index))${SHUNPO_RESET}] /$(basename ${parent_dirs[i]})"
207 | fi
208 | done
209 |
210 | if [ $last_page -gt 1 ]; then
211 | echo -e "${SHUNPO_CYAN}[$((current_page + 1)) / $last_page]${SHUNPO_RESET}"
212 | fi
213 |
214 | # Read and process user input.
215 | read -rsn1 input
216 | if [[ $input == "n" ]]; then
217 | if [ $((current_page + 1)) -lt "$last_page" ]; then
218 | current_page=$((current_page + 1))
219 | fi
220 | shunpo_clear_output
221 |
222 | elif [[ $input == "p" ]]; then
223 | if [ $((current_page - 1)) -ge 0 ]; then
224 | current_page=$((current_page - 1))
225 | fi
226 | shunpo_clear_output
227 |
228 | elif [[ $input =~ ^[0-9]+$ ]] && [ "$input" -gt 0 ] && [ "$input" -le "$max_per_page" ]; then
229 | selected_index=$((start_index + input))
230 | if [[ $selected_index -lt $total_parents ]]; then
231 | shunpo_clear_output
232 | tput cnorm
233 | cd "${parent_dirs[$selected_index]}" || exit
234 | echo -e "${SHUNPO_GREEN}${SHUNPO_BOLD}Changed to:${SHUNPO_RESET} ${parent_dirs[$selected_index]}"
235 | return 0
236 | else
237 | shunpo_clear_output
238 | fi
239 | else
240 | shunpo_clear_output
241 | tput cnorm
242 | return 0
243 | fi
244 | done
245 | shunpo_clear_output
246 | tput cnorm
247 | return 0
248 | }
249 |
250 | function shunpo_jump_to_child_dir() {
251 | local current_dir=$(pwd)
252 | local max_per_page=10
253 | local current_page=0
254 | local last_page
255 | local start_index
256 | local end_index
257 | local child_dirs=()
258 | local cache_index
259 | local selected_path="$current_dir" # selected path gets updated in each iteration.
260 | local start_dir=$(realpath "$current_dir")
261 | local is_start_dir=1
262 | local total_child_dirs
263 | local padding_lines
264 |
265 | tput civis
266 |
267 | # Cache previously traversed directories.
268 | local cache_keys=()
269 | local cache_values=()
270 |
271 | function shunpo_is_cached() {
272 | local path="$1"
273 | for i in "${!cache_keys[@]}"; do
274 | if [[ ${cache_keys[$i]} == "$path" ]]; then
275 | echo "$i"
276 | return 0
277 | fi
278 | done
279 | return 1
280 | }
281 |
282 | while true; do
283 | # Attempt to retrieve from cache.
284 | if cache_index=$(shunpo_is_cached "$selected_path"); then
285 | # Use cached value.
286 | IFS='|' read -r -a child_dirs <<<"${cache_values[$cache_index]}"
287 | else
288 | # Collect directories if not cached.
289 | child_dirs=()
290 | while IFS= read -r dir; do
291 | child_dirs+=("$dir")
292 | done < <(find "$selected_path" -maxdepth 1 -mindepth 1 -type d | sort)
293 |
294 | # Add to cache.
295 | cache_keys+=("$selected_path")
296 | cache_values+=("$(
297 | IFS='|'
298 | echo "${child_dirs[*]}"
299 | )")
300 | fi
301 | total_child_dirs=${#child_dirs[@]}
302 |
303 | # Determine index range to diplay.
304 | start_index=$((current_page * max_per_page))
305 | end_index=$((start_index + max_per_page))
306 | if [ "$end_index" -gt "$total_child_dirs" ]; then
307 | end_index=$total_child_dirs
308 | fi
309 |
310 | last_page=$(((total_child_dirs + max_per_page - 1) / max_per_page))
311 |
312 | # Pad the bottom of the terminal to avoid erroneous printing.
313 | if [ "$max_per_page" -lt "$total_child_dirs" ]; then
314 | padding_lines=$max_per_page
315 | else
316 | padding_lines=$total_child_dirs
317 | fi
318 |
319 | if [ $last_page -gt 1 ]; then
320 | padding_lines=$((padding_lines + 1)) # page numbers.
321 | fi
322 |
323 | padding_lines=$((padding_lines + 2)) # header and selected path.
324 | shunpo_add_space $padding_lines
325 |
326 | tput sc
327 | echo -e "${SHUNPO_BOLD}${SHUNPO_CYAN}Shunpo ${SHUNPO_RESET}"
328 |
329 | # Print selected path and options.
330 | if [[ $is_start_dir -eq 1 ]]; then
331 | echo -e "Selected Path: ${SHUNPO_CYAN}$selected_path${SHUNPO_RESET} ${SHUNPO_ORANGE}(Initial)${SHUNPO_RESET}"
332 | else
333 | echo -e "Selected Path: ${SHUNPO_CYAN}$selected_path${SHUNPO_RESET}"
334 | fi
335 |
336 | if [[ $total_child_dirs -eq 0 ]]; then
337 | if [[ $is_start_dir -eq 1 ]]; then
338 | shunpo_clear_output
339 | echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}No Child Directories.${SHUNPO_RESET}"
340 | tput cnorm
341 | return 1
342 | else
343 | echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}No Child Directories.${SHUNPO_RESET}"
344 | fi
345 | else
346 | # Print child directories.
347 | for ((i = start_index; i < end_index; i++)); do
348 | echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}$((i - start_index))${SHUNPO_RESET}] ${child_dirs[i]#$selected_path}"
349 | done
350 |
351 | if [ $last_page -gt 1 ]; then
352 | echo -e "${SHUNPO_CYAN}[$((current_page + 1)) / $last_page]${SHUNPO_RESET}"
353 | fi
354 | fi
355 |
356 | # Process input.
357 | read -rsn1 input
358 | if [[ $input == "n" ]]; then
359 | if [ $((current_page + 1)) -lt "$last_page" ]; then
360 | current_page=$((current_page + 1))
361 | fi
362 | shunpo_clear_output
363 |
364 | elif [[ $input == "p" ]]; then
365 | if [ $((current_page - 1)) -ge 0 ]; then
366 | current_page=$((current_page - 1))
367 | fi
368 | shunpo_clear_output
369 |
370 | elif [[ $input == "b" ]]; then
371 | if [[ $is_start_dir -eq 1 ]]; then
372 | shunpo_clear_output
373 | continue
374 | else
375 | selected_path=$(realpath "$selected_path/../")
376 | fi
377 |
378 | if [[ $selected_path == "$start_dir" ]]; then
379 | is_start_dir=1
380 | else
381 | is_start_dir=0
382 | fi
383 | current_page=0
384 | shunpo_clear_output
385 |
386 | elif [[ $input == "" ]]; then
387 | shunpo_clear_output
388 | if [[ $is_start_dir -ne 1 ]]; then
389 | cd "$selected_path" || exit
390 | echo -e "${SHUNPO_GREEN}${SHUNPO_BOLD}Changed to:${SHUNPO_RESET} $selected_path"
391 | fi
392 | break
393 |
394 | elif [[ $input =~ ^[0-9]+$ ]] && [[ $input -ge 0 ]] && [[ $input -lt $max_per_page ]]; then
395 | selected_index=$((start_index + input))
396 | if [[ $selected_index -lt $total_child_dirs ]]; then
397 | selected_path="${child_dirs[selected_index]}"
398 | is_start_dir=0
399 | current_page=0
400 | fi
401 | shunpo_clear_output
402 |
403 | else
404 | shunpo_clear_output
405 | tput cnorm
406 | break
407 | fi
408 | done
409 | tput cnorm
410 | return 0
411 | }
412 |
413 | # Function to open several lines of space before writing when near the end of the terminal
414 | # to avoid visual issues.
415 | function shunpo_add_space() {
416 | # Get total terminal lines.
417 | total_lines=$(tput lines)
418 |
419 | # Fetch the current cursor row position using ANSI escape codes.
420 | cursor_line=$(IFS=';' read -rsdR -p $'\033[6n' -a pos && echo "${pos[0]#*[}")
421 |
422 | # Calculate lines from current position to bottom.
423 | lines_to_bottom=$((total_lines - cursor_line))
424 | if [ "$lines_to_bottom" -lt "$1" ]; then
425 | for ((i = 0; i < $1; i++)); do
426 | echo
427 | done
428 | tput cuu "$1"
429 | fi
430 | tput ed
431 | }
432 |
433 | function shunpo_clear_output() {
434 | tput rc # Restore saved cursor position.
435 | tput ed # Clear everything below the cursor.
436 | }
437 |
438 | function shunpo_assert_bookmarks_exist() {
439 | # Ensure the bookmarks file exists and is not empty.
440 | if [ ! -f "$SHUNPO_BOOKMARKS_FILE" ] || [ ! -s "$SHUNPO_BOOKMARKS_FILE" ]; then
441 | echo -e "${SHUNPO_ORANGE}${SHUNPO_BOLD}No Bookmarks Found.${SHUNPO_RESET}"
442 | shunpo_cleanup
443 | return 1
444 | fi
445 | }
446 |
447 | function shunpo_cleanup() {
448 | # Clean up to avoid namespace pollution.
449 | unset SHUNPO_BOOKMARKS_FILE
450 | unset SHUNPO_BOOKMARKS_DIR
451 | unset IFS
452 | unset shunpo_selected_dir
453 | unset shunpo_selected_bookmark_index
454 | unset -f shunpo_interact_bookmarks
455 | unset -f shunpo_add_space
456 | unset -f shunpo_clear_output
457 | unset -f shunpo_assert_bookmarks_exist
458 | unset -f shunpo_jump_to_parent_dir
459 | unset -f shunpo_jump_to_child_dir
460 | unset -f shunpo_is_cached
461 | unset -f shunpo_handle_kill
462 | unset -f shunpo_cleanup
463 | tput cnorm
464 | stty echo
465 | trap - SIGINT
466 | }
467 |
--------------------------------------------------------------------------------
/src/go_to_bookmark.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This script should be sourced and not executed.
4 |
5 | # Colors and formatting.
6 | SHUNPO_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7 | source "$SHUNPO_SCRIPT_DIR"/colors.sh
8 | source "$SHUNPO_SCRIPT_DIR"/functions.sh
9 |
10 | function shunpo_handle_kill() {
11 | shunpo_clear_output
12 | if declare -f shunpo_cleanup >/dev/null; then
13 | shunpo_cleanup
14 | fi
15 | return 1
16 | }
17 |
18 | trap 'shunpo_handle_kill; return 1' SIGINT
19 |
20 | if ! shunpo_assert_bookmarks_exist; then
21 | return 1
22 | fi
23 |
24 | shunpo_interact_bookmarks "Go To Bookmark" "$1"
25 |
26 | # Handle case where bookmark is not set.
27 | if [ $? -eq 2 ]; then
28 | if declare -f shunpo_cleanup >/dev/null; then
29 | shunpo_cleanup
30 | fi
31 | echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}Bookmark is Empty.${SHUNPO_RESET}"
32 | return 1
33 | fi
34 |
35 | if [[ -z $shunpo_selected_dir ]]; then
36 | if declare -f shunpo_cleanup >/dev/null; then
37 | shunpo_cleanup
38 | fi
39 | return 1
40 |
41 | elif [[ -d $shunpo_selected_dir ]]; then
42 | if cd "$shunpo_selected_dir"; then
43 | echo -e "${SHUNPO_GREEN}${SHUNPO_BOLD}Changed to:${SHUNPO_RESET} $shunpo_selected_dir"
44 | else
45 | echo -e "${SHUNPO_RED}${SHUNPO_BOLD}Directory does not exist:${SHUNPO_RESET} $shunpo_selected_dir"
46 | fi
47 |
48 | else
49 | echo -e "${SHUNPO_RED}${SHUNPO_BOLD}Directory does not exist:${SHUNPO_RESET} $shunpo_selected_dir"
50 | fi
51 |
52 | shunpo_cleanup
53 |
--------------------------------------------------------------------------------
/src/jump_to_child.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This script should be sourced and not executed.
4 |
5 | SHUNPO_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6 | source "$SHUNPO_SCRIPT_DIR"/colors.sh
7 | source "$SHUNPO_SCRIPT_DIR"/functions.sh
8 |
9 | function shunpo_handle_kill() {
10 | shunpo_clear_output
11 | if declare -f shunpo_cleanup >/dev/null; then
12 | shunpo_cleanup
13 | fi
14 | return 0
15 | }
16 |
17 | trap 'shunpo_handle_kill; return 1' SIGINT
18 |
19 | shunpo_jump_to_child_dir
20 | if declare -f shunpo_cleanup >/dev/null; then
21 | shunpo_cleanup
22 | fi
23 |
--------------------------------------------------------------------------------
/src/jump_to_parent.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This script should be sourced and not executed.
4 |
5 | SHUNPO_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6 | source "$SHUNPO_SCRIPT_DIR"/colors.sh
7 | source "$SHUNPO_SCRIPT_DIR"/functions.sh
8 |
9 | function shunpo_handle_kill() {
10 | shunpo_clear_output
11 | if declare -f shunpo_cleanup >/dev/null; then
12 | shunpo_cleanup
13 | fi
14 | return 1
15 | }
16 |
17 | trap 'shunpo_handle_kill; return 1' SIGINT
18 |
19 | shunpo_jump_to_parent_dir $1
20 |
21 | # Handle case where bookmark is not set.
22 | if [ $? -eq 1 ]; then
23 |
24 | if declare -f shunpo_cleanup >/dev/null; then
25 | shunpo_cleanup
26 | fi
27 | return 1
28 | elif [ $? -eq 2 ]; then
29 | if declare -f shunpo_cleanup >/dev/null; then
30 | shunpo_cleanup
31 | fi
32 | echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}Invalid Parent Selection.${SHUNPO_RESET}"
33 | return 1
34 | fi
35 |
36 | if declare -f shunpo_cleanup >/dev/null; then
37 | shunpo_cleanup
38 | fi
39 |
--------------------------------------------------------------------------------
/src/list_bookmarks.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Colors and formatting
4 | SHUNPO_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5 | source "$SHUNPO_SCRIPT_DIR"/colors.sh
6 | source "$SHUNPO_SCRIPT_DIR"/functions.sh
7 |
8 | function shunpo_handle_kill() {
9 | shunpo_clear_output
10 | shunpo_cleanup
11 | exit 1
12 | }
13 |
14 | trap 'shunpo_handle_kill' SIGINT
15 |
16 | if ! shunpo_assert_bookmarks_exist; then
17 | exit 1
18 | fi
19 |
20 | shunpo_interact_bookmarks "List Bookmarks"
21 | shunpo_cleanup
22 |
--------------------------------------------------------------------------------
/src/remove_bookmark.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Colors and formatting.
4 | SHUNPO_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5 | source "$SHUNPO_SCRIPT_DIR/colors.sh"
6 | source "$SHUNPO_SCRIPT_DIR/functions.sh"
7 |
8 | function shunpo_handle_kill() {
9 | shunpo_clear_output
10 | shunpo_cleanup
11 | exit 1
12 | }
13 |
14 | trap 'shunpo_handle_kill' SIGINT
15 |
16 | # Check if bookmarks exist.
17 | if ! shunpo_assert_bookmarks_exist; then
18 | exit 1
19 | fi
20 |
21 | shunpo_interact_bookmarks "Remove Bookmarks" $1
22 |
23 | # Handle case where bookmark is not set. Corresponds to return code 2.
24 | if [ $? -eq 2 ]; then
25 | if declare -f shunpo_cleanup >/dev/null; then
26 | shunpo_cleanup
27 | fi
28 | echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}Bookmark is Empty.${SHUNPO_RESET}"
29 | exit 1
30 | fi
31 |
32 | bookmarks=()
33 | while IFS= read -r bookmark; do
34 | bookmarks+=("$bookmark")
35 | done <"$SHUNPO_BOOKMARKS_FILE"
36 |
37 | if [[ -z $shunpo_selected_dir ]]; then
38 | unset shunpo_selected_dir
39 | exit 1
40 |
41 | elif [[ $shunpo_selected_bookmark_index -ge 0 ]] && [[ $shunpo_selected_bookmark_index -lt ${#bookmarks[@]} ]]; then
42 | # Remove the selected bookmark from the file.
43 | awk -v dir="$shunpo_selected_dir" '$0 != dir' "$SHUNPO_BOOKMARKS_FILE" >"${SHUNPO_BOOKMARKS_FILE}.tmp" && mv "${SHUNPO_BOOKMARKS_FILE}.tmp" "$SHUNPO_BOOKMARKS_FILE"
44 |
45 | # Display the removed bookmark message.
46 | echo -e "${SHUNPO_RED}${SHUNPO_BOLD}Removed bookmark:${SHUNPO_RESET} $shunpo_selected_dir"
47 |
48 | # Delete the bookmarks file if it is empty.
49 | if [ ! -s "$SHUNPO_BOOKMARKS_FILE" ]; then
50 | rm -f "$SHUNPO_BOOKMARKS_FILE"
51 | fi
52 | else
53 | exit 1
54 | fi
55 |
56 | shunpo_cleanup
57 |
--------------------------------------------------------------------------------
/tests/bats/Notes.md:
--------------------------------------------------------------------------------
1 | Files in this folder were copied from:
2 | https://github.com/ztombol/bats-assert
3 | https://github.com/ztombol/bats-support
4 |
--------------------------------------------------------------------------------
/tests/bats/assert.sh:
--------------------------------------------------------------------------------
1 | #
2 | # bats-assert - Common assertions for Bats
3 | #
4 | # Written in 2016 by Zoltan Tombol
5 | #
6 | # To the extent possible under law, the author(s) have dedicated all
7 | # copyright and related and neighboring rights to this software to the
8 | # public domain worldwide. This software is distributed without any
9 | # warranty.
10 | #
11 | # You should have received a copy of the CC0 Public Domain Dedication
12 | # along with this software. If not, see
13 | # .
14 | #
15 |
16 | #
17 | # assert.bash
18 | # -----------
19 | #
20 | # Assertions are functions that perform a test and output relevant
21 | # information on failure to help debugging. They return 1 on failure
22 | # and 0 otherwise.
23 | #
24 | # All output is formatted for readability using the functions of
25 | # `output.bash' and sent to the standard error.
26 | #
27 |
28 | # Fail and display the expression if it evaluates to false.
29 | #
30 | # NOTE: The expression must be a simple command. Compound commands, such
31 | # as `[[', can be used only when executed with `bash -c'.
32 | #
33 | # Globals:
34 | # none
35 | # Arguments:
36 | # $1 - expression
37 | # Returns:
38 | # 0 - expression evaluates to TRUE
39 | # 1 - otherwise
40 | # Outputs:
41 | # STDERR - details, on failure
42 | assert() {
43 | if ! "$@"; then
44 | batslib_print_kv_single 10 'expression' "$*" |
45 | batslib_decorate 'assertion failed' |
46 | fail
47 | fi
48 | }
49 |
50 | # Fail and display the expression if it evaluates to true.
51 | #
52 | # NOTE: The expression must be a simple command. Compound commands, such
53 | # as `[[', can be used only when executed with `bash -c'.
54 | #
55 | # Globals:
56 | # none
57 | # Arguments:
58 | # $1 - expression
59 | # Returns:
60 | # 0 - expression evaluates to FALSE
61 | # 1 - otherwise
62 | # Outputs:
63 | # STDERR - details, on failure
64 | refute() {
65 | if "$@"; then
66 | batslib_print_kv_single 10 'expression' "$*" |
67 | batslib_decorate 'assertion succeeded, but it was expected to fail' |
68 | fail
69 | fi
70 | }
71 |
72 | # Fail and display details if the expected and actual values do not
73 | # equal. Details include both values.
74 | #
75 | # Globals:
76 | # none
77 | # Arguments:
78 | # $1 - actual value
79 | # $2 - expected value
80 | # Returns:
81 | # 0 - values equal
82 | # 1 - otherwise
83 | # Outputs:
84 | # STDERR - details, on failure
85 | assert_equal() {
86 | if [[ $1 != "$2" ]]; then
87 | batslib_print_kv_single_or_multi 8 \
88 | 'expected' "$2" \
89 | 'actual' "$1" |
90 | batslib_decorate 'values do not equal' |
91 | fail
92 | fi
93 | }
94 |
95 | # Fail and display details if `$status' is not 0. Details include
96 | # `$status' and `$output'.
97 | #
98 | # Globals:
99 | # status
100 | # output
101 | # Arguments:
102 | # none
103 | # Returns:
104 | # 0 - `$status' is 0
105 | # 1 - otherwise
106 | # Outputs:
107 | # STDERR - details, on failure
108 | assert_success() {
109 | if ((status != 0)); then
110 | {
111 | local -ir width=6
112 | batslib_print_kv_single "$width" 'status' "$status"
113 | batslib_print_kv_single_or_multi "$width" 'output' "$output"
114 | } | batslib_decorate 'command failed' |
115 | fail
116 | fi
117 | }
118 |
119 | # Fail and display details if `$status' is 0. Details include `$output'.
120 | #
121 | # Optionally, when the expected status is specified, fail when it does
122 | # not equal `$status'. In this case, details include the expected and
123 | # actual status, and `$output'.
124 | #
125 | # Globals:
126 | # status
127 | # output
128 | # Arguments:
129 | # $1 - [opt] expected status
130 | # Returns:
131 | # 0 - `$status' is not 0, or
132 | # `$status' equals the expected status
133 | # 1 - otherwise
134 | # Outputs:
135 | # STDERR - details, on failure
136 | assert_failure() {
137 | (($# > 0)) && local -r expected="$1"
138 | if ((status == 0)); then
139 | batslib_print_kv_single_or_multi 6 'output' "$output" |
140 | batslib_decorate 'command succeeded, but it was expected to fail' |
141 | fail
142 | elif (($# > 0)) && ((status != expected)); then
143 | {
144 | local -ir width=8
145 | batslib_print_kv_single "$width" \
146 | 'expected' "$expected" \
147 | 'actual' "$status"
148 | batslib_print_kv_single_or_multi "$width" \
149 | 'output' "$output"
150 | } | batslib_decorate 'command failed as expected, but status differs' |
151 | fail
152 | fi
153 | }
154 |
155 | # Fail and display details if `$output' does not match the expected
156 | # output. The expected output can be specified either by the first
157 | # parameter or on the standard input.
158 | #
159 | # By default, literal matching is performed. The assertion fails if the
160 | # expected output does not equal `$output'. Details include both values.
161 | #
162 | # Option `--partial' enables partial matching. The assertion fails if
163 | # the expected substring cannot be found in `$output'.
164 | #
165 | # Option `--regexp' enables regular expression matching. The assertion
166 | # fails if the extended regular expression does not match `$output'. An
167 | # invalid regular expression causes an error to be displayed.
168 | #
169 | # It is an error to use partial and regular expression matching
170 | # simultaneously.
171 | #
172 | # Globals:
173 | # output
174 | # Options:
175 | # -p, --partial - partial matching
176 | # -e, --regexp - extended regular expression matching
177 | # -, --stdin - read expected output from the standard input
178 | # Arguments:
179 | # $1 - expected output
180 | # Returns:
181 | # 0 - expected matches the actual output
182 | # 1 - otherwise
183 | # Inputs:
184 | # STDIN - [=$1] expected output
185 | # Outputs:
186 | # STDERR - details, on failure
187 | # error message, on error
188 | assert_output() {
189 | local -i is_mode_partial=0
190 | local -i is_mode_regexp=0
191 | local -i is_mode_nonempty=0
192 | local -i use_stdin=0
193 |
194 | # Handle options.
195 | if (($# == 0)); then
196 | is_mode_nonempty=1
197 | fi
198 |
199 | while (($# > 0)); do
200 | case "$1" in
201 | -p | --partial)
202 | is_mode_partial=1
203 | shift
204 | ;;
205 | -e | --regexp)
206 | is_mode_regexp=1
207 | shift
208 | ;;
209 | - | --stdin)
210 | use_stdin=1
211 | shift
212 | ;;
213 | --)
214 | shift
215 | break
216 | ;;
217 | *) break ;;
218 | esac
219 | done
220 |
221 | if ((is_mode_partial)) && ((is_mode_regexp)); then
222 | echo "\`--partial' and \`--regexp' are mutually exclusive" |
223 | batslib_decorate 'ERROR: assert_output' |
224 | fail
225 | return $?
226 | fi
227 |
228 | # Arguments.
229 | local expected
230 | if ((use_stdin)); then
231 | expected="$(cat -)"
232 | else
233 | expected="$1"
234 | fi
235 |
236 | # Matching.
237 | if ((is_mode_nonempty)); then
238 | if [ -z "$output" ]; then
239 | echo 'expected non-empty output, but output was empty' |
240 | batslib_decorate 'no output' |
241 | fail
242 | fi
243 | elif ((is_mode_regexp)); then
244 | if [[ '' =~ $expected ]] || (($? == 2)); then
245 | echo "Invalid extended regular expression: \`$expected'" |
246 | batslib_decorate 'ERROR: assert_output' |
247 | fail
248 | elif ! [[ $output =~ $expected ]]; then
249 | batslib_print_kv_single_or_multi 6 \
250 | 'regexp' "$expected" \
251 | 'output' "$output" |
252 | batslib_decorate 'regular expression does not match output' |
253 | fail
254 | fi
255 | elif ((is_mode_partial)); then
256 | if [[ $output != *"$expected"* ]]; then
257 | batslib_print_kv_single_or_multi 9 \
258 | 'substring' "$expected" \
259 | 'output' "$output" |
260 | batslib_decorate 'output does not contain substring' |
261 | fail
262 | fi
263 | else
264 | if [[ $output != "$expected" ]]; then
265 | batslib_print_kv_single_or_multi 8 \
266 | 'expected' "$expected" \
267 | 'actual' "$output" |
268 | batslib_decorate 'output differs' |
269 | fail
270 | fi
271 | fi
272 | }
273 |
274 | # Fail and display details if `$output' matches the unexpected output.
275 | # The unexpected output can be specified either by the first parameter
276 | # or on the standard input.
277 | #
278 | # By default, literal matching is performed. The assertion fails if the
279 | # unexpected output equals `$output'. Details include `$output'.
280 | #
281 | # Option `--partial' enables partial matching. The assertion fails if
282 | # the unexpected substring is found in `$output'. The unexpected
283 | # substring is added to details.
284 | #
285 | # Option `--regexp' enables regular expression matching. The assertion
286 | # fails if the extended regular expression does matches `$output'. The
287 | # regular expression is added to details. An invalid regular expression
288 | # causes an error to be displayed.
289 | #
290 | # It is an error to use partial and regular expression matching
291 | # simultaneously.
292 | #
293 | # Globals:
294 | # output
295 | # Options:
296 | # -p, --partial - partial matching
297 | # -e, --regexp - extended regular expression matching
298 | # -, --stdin - read unexpected output from the standard input
299 | # Arguments:
300 | # $1 - unexpected output
301 | # Returns:
302 | # 0 - unexpected matches the actual output
303 | # 1 - otherwise
304 | # Inputs:
305 | # STDIN - [=$1] unexpected output
306 | # Outputs:
307 | # STDERR - details, on failure
308 | # error message, on error
309 | refute_output() {
310 | local -i is_mode_partial=0
311 | local -i is_mode_regexp=0
312 | local -i is_mode_empty=0
313 | local -i use_stdin=0
314 |
315 | # Handle options.
316 | if (($# == 0)); then
317 | is_mode_empty=1
318 | fi
319 |
320 | while (($# > 0)); do
321 | case "$1" in
322 | -p | --partial)
323 | is_mode_partial=1
324 | shift
325 | ;;
326 | -e | --regexp)
327 | is_mode_regexp=1
328 | shift
329 | ;;
330 | - | --stdin)
331 | use_stdin=1
332 | shift
333 | ;;
334 | --)
335 | shift
336 | break
337 | ;;
338 | *) break ;;
339 | esac
340 | done
341 |
342 | if ((is_mode_partial)) && ((is_mode_regexp)); then
343 | echo "\`--partial' and \`--regexp' are mutually exclusive" |
344 | batslib_decorate 'ERROR: refute_output' |
345 | fail
346 | return $?
347 | fi
348 |
349 | # Arguments.
350 | local unexpected
351 | if ((use_stdin)); then
352 | unexpected="$(cat -)"
353 | else
354 | unexpected="$1"
355 | fi
356 |
357 | if ((is_mode_regexp == 1)) && [[ '' =~ $unexpected ]] || (($? == 2)); then
358 | echo "Invalid extended regular expression: \`$unexpected'" |
359 | batslib_decorate 'ERROR: refute_output' |
360 | fail
361 | return $?
362 | fi
363 |
364 | # Matching.
365 | if ((is_mode_empty)); then
366 | if [ -n "$output" ]; then
367 | batslib_print_kv_single_or_multi 6 \
368 | 'output' "$output" |
369 | batslib_decorate 'output non-empty, but expected no output' |
370 | fail
371 | fi
372 | elif ((is_mode_regexp)); then
373 | if [[ $output =~ $unexpected ]] || (($? == 0)); then
374 | batslib_print_kv_single_or_multi 6 \
375 | 'regexp' "$unexpected" \
376 | 'output' "$output" |
377 | batslib_decorate 'regular expression should not match output' |
378 | fail
379 | fi
380 | elif ((is_mode_partial)); then
381 | if [[ $output == *"$unexpected"* ]]; then
382 | batslib_print_kv_single_or_multi 9 \
383 | 'substring' "$unexpected" \
384 | 'output' "$output" |
385 | batslib_decorate 'output should not contain substring' |
386 | fail
387 | fi
388 | else
389 | if [[ $output == "$unexpected" ]]; then
390 | batslib_print_kv_single_or_multi 6 \
391 | 'output' "$output" |
392 | batslib_decorate 'output equals, but it was expected to differ' |
393 | fail
394 | fi
395 | fi
396 | }
397 |
398 | # Fail and display details if the expected line is not found in the
399 | # output (default) or in a specific line of it.
400 | #
401 | # By default, the entire output is searched for the expected line. The
402 | # expected line is matched against every element of `${lines[@]}'. If no
403 | # match is found, the assertion fails. Details include the expected line
404 | # and `${lines[@]}'.
405 | #
406 | # When `--index ' is specified, only the -th line is matched.
407 | # If the expected line does not match `${lines[]}', the assertion
408 | # fails. Details include and the compared lines.
409 | #
410 | # By default, literal matching is performed. A literal match fails if
411 | # the expected string does not equal the matched string.
412 | #
413 | # Option `--partial' enables partial matching. A partial match fails if
414 | # the expected substring is not found in the target string.
415 | #
416 | # Option `--regexp' enables regular expression matching. A regular
417 | # expression match fails if the extended regular expression does not
418 | # match the target string. An invalid regular expression causes an error
419 | # to be displayed.
420 | #
421 | # It is an error to use partial and regular expression matching
422 | # simultaneously.
423 | #
424 | # Mandatory arguments to long options are mandatory for short options
425 | # too.
426 | #
427 | # Globals:
428 | # output
429 | # lines
430 | # Options:
431 | # -n, --index - match the -th line
432 | # -p, --partial - partial matching
433 | # -e, --regexp - extended regular expression matching
434 | # Arguments:
435 | # $1 - expected line
436 | # Returns:
437 | # 0 - match found
438 | # 1 - otherwise
439 | # Outputs:
440 | # STDERR - details, on failure
441 | # error message, on error
442 | # FIXME(ztombol): Display `${lines[@]}' instead of `$output'!
443 | assert_line() {
444 | local -i is_match_line=0
445 | local -i is_mode_partial=0
446 | local -i is_mode_regexp=0
447 |
448 | # Handle options.
449 | while (($# > 0)); do
450 | case "$1" in
451 | -n | --index)
452 | if (($# < 2)) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
453 | echo "\`--index' requires an integer argument: \`$2'" |
454 | batslib_decorate 'ERROR: assert_line' |
455 | fail
456 | return $?
457 | fi
458 | is_match_line=1
459 | local -ri idx="$2"
460 | shift 2
461 | ;;
462 | -p | --partial)
463 | is_mode_partial=1
464 | shift
465 | ;;
466 | -e | --regexp)
467 | is_mode_regexp=1
468 | shift
469 | ;;
470 | --)
471 | shift
472 | break
473 | ;;
474 | *) break ;;
475 | esac
476 | done
477 |
478 | if ((is_mode_partial)) && ((is_mode_regexp)); then
479 | echo "\`--partial' and \`--regexp' are mutually exclusive" |
480 | batslib_decorate 'ERROR: assert_line' |
481 | fail
482 | return $?
483 | fi
484 |
485 | # Arguments.
486 | local -r expected="$1"
487 |
488 | if ((is_mode_regexp == 1)) && [[ '' =~ $expected ]] || (($? == 2)); then
489 | echo "Invalid extended regular expression: \`$expected'" |
490 | batslib_decorate 'ERROR: assert_line' |
491 | fail
492 | return $?
493 | fi
494 |
495 | # Matching.
496 | if ((is_match_line)); then
497 | # Specific line.
498 | if ((is_mode_regexp)); then
499 | if ! [[ ${lines[$idx]} =~ $expected ]]; then
500 | batslib_print_kv_single 6 \
501 | 'index' "$idx" \
502 | 'regexp' "$expected" \
503 | 'line' "${lines[$idx]}" |
504 | batslib_decorate 'regular expression does not match line' |
505 | fail
506 | fi
507 | elif ((is_mode_partial)); then
508 | if [[ ${lines[$idx]} != *"$expected"* ]]; then
509 | batslib_print_kv_single 9 \
510 | 'index' "$idx" \
511 | 'substring' "$expected" \
512 | 'line' "${lines[$idx]}" |
513 | batslib_decorate 'line does not contain substring' |
514 | fail
515 | fi
516 | else
517 | if [[ ${lines[$idx]} != "$expected" ]]; then
518 | batslib_print_kv_single 8 \
519 | 'index' "$idx" \
520 | 'expected' "$expected" \
521 | 'actual' "${lines[$idx]}" |
522 | batslib_decorate 'line differs' |
523 | fail
524 | fi
525 | fi
526 | else
527 | # Contained in output.
528 | if ((is_mode_regexp)); then
529 | local -i idx
530 | for ((idx = 0; idx < ${#lines[@]}; ++idx)); do
531 | [[ ${lines[$idx]} =~ $expected ]] && return 0
532 | done
533 | {
534 | local -ar single=(
535 | 'regexp' "$expected"
536 | )
537 | local -ar may_be_multi=(
538 | 'output' "$output"
539 | )
540 | local -ir width="$(batslib_get_max_single_line_key_width \
541 | "${single[@]}" "${may_be_multi[@]}")"
542 | batslib_print_kv_single "$width" "${single[@]}"
543 | batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
544 | } | batslib_decorate 'no output line matches regular expression' |
545 | fail
546 | elif ((is_mode_partial)); then
547 | local -i idx
548 | for ((idx = 0; idx < ${#lines[@]}; ++idx)); do
549 | [[ ${lines[$idx]} == *"$expected"* ]] && return 0
550 | done
551 | {
552 | local -ar single=(
553 | 'substring' "$expected"
554 | )
555 | local -ar may_be_multi=(
556 | 'output' "$output"
557 | )
558 | local -ir width="$(batslib_get_max_single_line_key_width \
559 | "${single[@]}" "${may_be_multi[@]}")"
560 | batslib_print_kv_single "$width" "${single[@]}"
561 | batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
562 | } | batslib_decorate 'no output line contains substring' |
563 | fail
564 | else
565 | local -i idx
566 | for ((idx = 0; idx < ${#lines[@]}; ++idx)); do
567 | [[ ${lines[$idx]} == "$expected" ]] && return 0
568 | done
569 | {
570 | local -ar single=(
571 | 'line' "$expected"
572 | )
573 | local -ar may_be_multi=(
574 | 'output' "$output"
575 | )
576 | local -ir width="$(batslib_get_max_single_line_key_width \
577 | "${single[@]}" "${may_be_multi[@]}")"
578 | batslib_print_kv_single "$width" "${single[@]}"
579 | batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
580 | } | batslib_decorate 'output does not contain line' |
581 | fail
582 | fi
583 | fi
584 | }
585 |
586 | # Fail and display details if the unexpected line is found in the output
587 | # (default) or in a specific line of it.
588 | #
589 | # By default, the entire output is searched for the unexpected line. The
590 | # unexpected line is matched against every element of `${lines[@]}'. If
591 | # a match is found, the assertion fails. Details include the unexpected
592 | # line, the index of the first match and `${lines[@]}' with the matching
593 | # line highlighted if `${lines[@]}' is longer than one line.
594 | #
595 | # When `--index ' is specified, only the -th line is matched.
596 | # If the unexpected line matches `${lines[]}', the assertion fails.
597 | # Details include and the unexpected line.
598 | #
599 | # By default, literal matching is performed. A literal match fails if
600 | # the unexpected string does not equal the matched string.
601 | #
602 | # Option `--partial' enables partial matching. A partial match fails if
603 | # the unexpected substring is found in the target string. When used with
604 | # `--index ', the unexpected substring is also displayed on
605 | # failure.
606 | #
607 | # Option `--regexp' enables regular expression matching. A regular
608 | # expression match fails if the extended regular expression matches the
609 | # target string. When used with `--index ', the regular expression
610 | # is also displayed on failure. An invalid regular expression causes an
611 | # error to be displayed.
612 | #
613 | # It is an error to use partial and regular expression matching
614 | # simultaneously.
615 | #
616 | # Mandatory arguments to long options are mandatory for short options
617 | # too.
618 | #
619 | # Globals:
620 | # output
621 | # lines
622 | # Options:
623 | # -n, --index - match the -th line
624 | # -p, --partial - partial matching
625 | # -e, --regexp - extended regular expression matching
626 | # Arguments:
627 | # $1 - unexpected line
628 | # Returns:
629 | # 0 - match not found
630 | # 1 - otherwise
631 | # Outputs:
632 | # STDERR - details, on failure
633 | # error message, on error
634 | # FIXME(ztombol): Display `${lines[@]}' instead of `$output'!
635 | refute_line() {
636 | local -i is_match_line=0
637 | local -i is_mode_partial=0
638 | local -i is_mode_regexp=0
639 |
640 | # Handle options.
641 | while (($# > 0)); do
642 | case "$1" in
643 | -n | --index)
644 | if (($# < 2)) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
645 | echo "\`--index' requires an integer argument: \`$2'" |
646 | batslib_decorate 'ERROR: refute_line' |
647 | fail
648 | return $?
649 | fi
650 | is_match_line=1
651 | local -ri idx="$2"
652 | shift 2
653 | ;;
654 | -p | --partial)
655 | is_mode_partial=1
656 | shift
657 | ;;
658 | -e | --regexp)
659 | is_mode_regexp=1
660 | shift
661 | ;;
662 | --)
663 | shift
664 | break
665 | ;;
666 | *) break ;;
667 | esac
668 | done
669 |
670 | if ((is_mode_partial)) && ((is_mode_regexp)); then
671 | echo "\`--partial' and \`--regexp' are mutually exclusive" |
672 | batslib_decorate 'ERROR: refute_line' |
673 | fail
674 | return $?
675 | fi
676 |
677 | # Arguments.
678 | local -r unexpected="$1"
679 |
680 | if ((is_mode_regexp == 1)) && [[ '' =~ $unexpected ]] || (($? == 2)); then
681 | echo "Invalid extended regular expression: \`$unexpected'" |
682 | batslib_decorate 'ERROR: refute_line' |
683 | fail
684 | return $?
685 | fi
686 |
687 | # Matching.
688 | if ((is_match_line)); then
689 | # Specific line.
690 | if ((is_mode_regexp)); then
691 | if [[ ${lines[$idx]} =~ $unexpected ]] || (($? == 0)); then
692 | batslib_print_kv_single 6 \
693 | 'index' "$idx" \
694 | 'regexp' "$unexpected" \
695 | 'line' "${lines[$idx]}" |
696 | batslib_decorate 'regular expression should not match line' |
697 | fail
698 | fi
699 | elif ((is_mode_partial)); then
700 | if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
701 | batslib_print_kv_single 9 \
702 | 'index' "$idx" \
703 | 'substring' "$unexpected" \
704 | 'line' "${lines[$idx]}" |
705 | batslib_decorate 'line should not contain substring' |
706 | fail
707 | fi
708 | else
709 | if [[ ${lines[$idx]} == "$unexpected" ]]; then
710 | batslib_print_kv_single 5 \
711 | 'index' "$idx" \
712 | 'line' "${lines[$idx]}" |
713 | batslib_decorate 'line should differ' |
714 | fail
715 | fi
716 | fi
717 | else
718 | # Line contained in output.
719 | if ((is_mode_regexp)); then
720 | local -i idx
721 | for ((idx = 0; idx < ${#lines[@]}; ++idx)); do
722 | if [[ ${lines[$idx]} =~ $unexpected ]]; then
723 | {
724 | local -ar single=(
725 | 'regexp' "$unexpected"
726 | 'index' "$idx"
727 | )
728 | local -a may_be_multi=(
729 | 'output' "$output"
730 | )
731 | local -ir width="$(batslib_get_max_single_line_key_width \
732 | "${single[@]}" "${may_be_multi[@]}")"
733 | batslib_print_kv_single "$width" "${single[@]}"
734 | if batslib_is_single_line "${may_be_multi[1]}"; then
735 | batslib_print_kv_single "$width" "${may_be_multi[@]}"
736 | else
737 | may_be_multi[1]="$(printf '%s' "${may_be_multi[1]}" |
738 | batslib_prefix |
739 | batslib_mark '>' "$idx")"
740 | batslib_print_kv_multi "${may_be_multi[@]}"
741 | fi
742 | } | batslib_decorate 'no line should match the regular expression' |
743 | fail
744 | return $?
745 | fi
746 | done
747 | elif ((is_mode_partial)); then
748 | local -i idx
749 | for ((idx = 0; idx < ${#lines[@]}; ++idx)); do
750 | if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
751 | {
752 | local -ar single=(
753 | 'substring' "$unexpected"
754 | 'index' "$idx"
755 | )
756 | local -a may_be_multi=(
757 | 'output' "$output"
758 | )
759 | local -ir width="$(batslib_get_max_single_line_key_width \
760 | "${single[@]}" "${may_be_multi[@]}")"
761 | batslib_print_kv_single "$width" "${single[@]}"
762 | if batslib_is_single_line "${may_be_multi[1]}"; then
763 | batslib_print_kv_single "$width" "${may_be_multi[@]}"
764 | else
765 | may_be_multi[1]="$(printf '%s' "${may_be_multi[1]}" |
766 | batslib_prefix |
767 | batslib_mark '>' "$idx")"
768 | batslib_print_kv_multi "${may_be_multi[@]}"
769 | fi
770 | } | batslib_decorate 'no line should contain substring' |
771 | fail
772 | return $?
773 | fi
774 | done
775 | else
776 | local -i idx
777 | for ((idx = 0; idx < ${#lines[@]}; ++idx)); do
778 | if [[ ${lines[$idx]} == "$unexpected" ]]; then
779 | {
780 | local -ar single=(
781 | 'line' "$unexpected"
782 | 'index' "$idx"
783 | )
784 | local -a may_be_multi=(
785 | 'output' "$output"
786 | )
787 | local -ir width="$(batslib_get_max_single_line_key_width \
788 | "${single[@]}" "${may_be_multi[@]}")"
789 | batslib_print_kv_single "$width" "${single[@]}"
790 | if batslib_is_single_line "${may_be_multi[1]}"; then
791 | batslib_print_kv_single "$width" "${may_be_multi[@]}"
792 | else
793 | may_be_multi[1]="$(printf '%s' "${may_be_multi[1]}" |
794 | batslib_prefix |
795 | batslib_mark '>' "$idx")"
796 | batslib_print_kv_multi "${may_be_multi[@]}"
797 | fi
798 | } | batslib_decorate 'line should not be in output' |
799 | fail
800 | return $?
801 | fi
802 | done
803 | fi
804 | fi
805 | }
806 |
--------------------------------------------------------------------------------
/tests/bats/error.sh:
--------------------------------------------------------------------------------
1 | #
2 | # bats-support - Supporting library for Bats test helpers
3 | #
4 | # Written in 2016 by Zoltan Tombol
5 | #
6 | # To the extent possible under law, the author(s) have dedicated all
7 | # copyright and related and neighboring rights to this software to the
8 | # public domain worldwide. This software is distributed without any
9 | # warranty.
10 | #
11 | # You should have received a copy of the CC0 Public Domain Dedication
12 | # along with this software. If not, see
13 | # .
14 | #
15 |
16 | #
17 | # error.bash
18 | # ----------
19 | #
20 | # Functions implementing error reporting. Used by public helper
21 | # functions or test suits directly.
22 | #
23 |
24 | # Fail and display a message. When no parameters are specified, the
25 | # message is read from the standard input. Other functions use this to
26 | # report failure.
27 | #
28 | # Globals:
29 | # none
30 | # Arguments:
31 | # $@ - [=STDIN] message
32 | # Returns:
33 | # 1 - always
34 | # Inputs:
35 | # STDIN - [=$@] message
36 | # Outputs:
37 | # STDERR - message
38 | fail() {
39 | (($# == 0)) && batslib_err || batslib_err "$@"
40 | return 1
41 | }
42 |
--------------------------------------------------------------------------------
/tests/bats/lang.sh:
--------------------------------------------------------------------------------
1 | #
2 | # bats-util - Various auxiliary functions for Bats
3 | #
4 | # Written in 2016 by Zoltan Tombol
5 | #
6 | # To the extent possible under law, the author(s) have dedicated all
7 | # copyright and related and neighboring rights to this software to the
8 | # public domain worldwide. This software is distributed without any
9 | # warranty.
10 | #
11 | # You should have received a copy of the CC0 Public Domain Dedication
12 | # along with this software. If not, see
13 | # .
14 | #
15 |
16 | #
17 | # lang.bash
18 | # ---------
19 | #
20 | # Bash language and execution related functions. Used by public helper
21 | # functions.
22 | #
23 |
24 | # Check whether the calling function was called from a given function.
25 | #
26 | # By default, direct invocation is checked. The function succeeds if the
27 | # calling function was called directly from the given function. In other
28 | # words, if the given function is the next element on the call stack.
29 | #
30 | # When `--indirect' is specified, indirect invocation is checked. The
31 | # function succeeds if the calling function was called from the given
32 | # function with any number of intermediate calls. In other words, if the
33 | # given function can be found somewhere on the call stack.
34 | #
35 | # Direct invocation is a form of indirect invocation with zero
36 | # intermediate calls.
37 | #
38 | # Globals:
39 | # FUNCNAME
40 | # Options:
41 | # -i, --indirect - check indirect invocation
42 | # Arguments:
43 | # $1 - calling function's name
44 | # Returns:
45 | # 0 - current function was called from the given function
46 | # 1 - otherwise
47 | batslib_is_caller() {
48 | local -i is_mode_direct=1
49 |
50 | # Handle options.
51 | while (($# > 0)); do
52 | case "$1" in
53 | -i | --indirect)
54 | is_mode_direct=0
55 | shift
56 | ;;
57 | --)
58 | shift
59 | break
60 | ;;
61 | *) break ;;
62 | esac
63 | done
64 |
65 | # Arguments.
66 | local -r func="$1"
67 |
68 | # Check call stack.
69 | if ((is_mode_direct)); then
70 | [[ $func == "${FUNCNAME[2]}" ]] && return 0
71 | else
72 | local -i depth
73 | for ((depth = 2; depth < ${#FUNCNAME[@]}; ++depth)); do
74 | [[ $func == "${FUNCNAME[$depth]}" ]] && return 0
75 | done
76 | fi
77 |
78 | return 1
79 | }
80 |
--------------------------------------------------------------------------------
/tests/bats/output.sh:
--------------------------------------------------------------------------------
1 | #
2 | # bats-support - Supporting library for Bats test helpers
3 | #
4 | # Written in 2016 by Zoltan Tombol
5 | #
6 | # To the extent possible under law, the author(s) have dedicated all
7 | # copyright and related and neighboring rights to this software to the
8 | # public domain worldwide. This software is distributed without any
9 | # warranty.
10 | #
11 | # You should have received a copy of the CC0 Public Domain Dedication
12 | # along with this software. If not, see
13 | # .
14 | #
15 |
16 | #
17 | # output.bash
18 | # -----------
19 | #
20 | # Private functions implementing output formatting. Used by public
21 | # helper functions.
22 | #
23 |
24 | # Print a message to the standard error. When no parameters are
25 | # specified, the message is read from the standard input.
26 | #
27 | # Globals:
28 | # none
29 | # Arguments:
30 | # $@ - [=STDIN] message
31 | # Returns:
32 | # none
33 | # Inputs:
34 | # STDIN - [=$@] message
35 | # Outputs:
36 | # STDERR - message
37 | batslib_err() {
38 | {
39 | if (($# > 0)); then
40 | echo "$@"
41 | else
42 | cat -
43 | fi
44 | } >&2
45 | }
46 |
47 | # Count the number of lines in the given string.
48 | #
49 | # TODO(ztombol): Fix tests and remove this note after #93 is resolved!
50 | # NOTE: Due to a bug in Bats, `batslib_count_lines "$output"' does not
51 | # give the same result as `${#lines[@]}' when the output contains
52 | # empty lines.
53 | # See PR #93 (https://github.com/sstephenson/bats/pull/93).
54 | #
55 | # Globals:
56 | # none
57 | # Arguments:
58 | # $1 - string
59 | # Returns:
60 | # none
61 | # Outputs:
62 | # STDOUT - number of lines
63 | batslib_count_lines() {
64 | local -i n_lines=0
65 | local line
66 | while IFS='' read -r line || [[ -n $line ]]; do
67 | ((++n_lines))
68 | done < <(printf '%s' "$1")
69 | echo "$n_lines"
70 | }
71 |
72 | # Determine whether all strings are single-line.
73 | #
74 | # Globals:
75 | # none
76 | # Arguments:
77 | # $@ - strings
78 | # Returns:
79 | # 0 - all strings are single-line
80 | # 1 - otherwise
81 | batslib_is_single_line() {
82 | for string in "$@"; do
83 | (($(batslib_count_lines "$string") > 1)) && return 1
84 | done
85 | return 0
86 | }
87 |
88 | # Determine the length of the longest key that has a single-line value.
89 | #
90 | # This function is useful in determining the correct width of the key
91 | # column in two-column format when some keys may have multi-line values
92 | # and thus should be excluded.
93 | #
94 | # Globals:
95 | # none
96 | # Arguments:
97 | # $odd - key
98 | # $even - value of the previous key
99 | # Returns:
100 | # none
101 | # Outputs:
102 | # STDOUT - length of longest key
103 | batslib_get_max_single_line_key_width() {
104 | local -i max_len=-1
105 | while (($# != 0)); do
106 | local -i key_len="${#1}"
107 | batslib_is_single_line "$2" && ((key_len > max_len)) && max_len="$key_len"
108 | shift 2
109 | done
110 | echo "$max_len"
111 | }
112 |
113 | # Print key-value pairs in two-column format.
114 | #
115 | # Keys are displayed in the first column, and their corresponding values
116 | # in the second. To evenly line up values, the key column is fixed-width
117 | # and its width is specified with the first parameter (possibly computed
118 | # using `batslib_get_max_single_line_key_width').
119 | #
120 | # Globals:
121 | # none
122 | # Arguments:
123 | # $1 - width of key column
124 | # $even - key
125 | # $odd - value of the previous key
126 | # Returns:
127 | # none
128 | # Outputs:
129 | # STDOUT - formatted key-value pairs
130 | batslib_print_kv_single() {
131 | local -ir col_width="$1"
132 | shift
133 | while (($# != 0)); do
134 | printf '%-*s : %s\n' "$col_width" "$1" "$2"
135 | shift 2
136 | done
137 | }
138 |
139 | # Print key-value pairs in multi-line format.
140 | #
141 | # The key is displayed first with the number of lines of its
142 | # corresponding value in parenthesis. Next, starting on the next line,
143 | # the value is displayed. For better readability, it is recommended to
144 | # indent values using `batslib_prefix'.
145 | #
146 | # Globals:
147 | # none
148 | # Arguments:
149 | # $odd - key
150 | # $even - value of the previous key
151 | # Returns:
152 | # none
153 | # Outputs:
154 | # STDOUT - formatted key-value pairs
155 | batslib_print_kv_multi() {
156 | while (($# != 0)); do
157 | printf '%s (%d lines):\n' "$1" "$(batslib_count_lines "$2")"
158 | printf '%s\n' "$2"
159 | shift 2
160 | done
161 | }
162 |
163 | # Print all key-value pairs in either two-column or multi-line format
164 | # depending on whether all values are single-line.
165 | #
166 | # If all values are single-line, print all pairs in two-column format
167 | # with the specified key column width (identical to using
168 | # `batslib_print_kv_single').
169 | #
170 | # Otherwise, print all pairs in multi-line format after indenting values
171 | # with two spaces for readability (identical to using `batslib_prefix'
172 | # and `batslib_print_kv_multi')
173 | #
174 | # Globals:
175 | # none
176 | # Arguments:
177 | # $1 - width of key column (for two-column format)
178 | # $even - key
179 | # $odd - value of the previous key
180 | # Returns:
181 | # none
182 | # Outputs:
183 | # STDOUT - formatted key-value pairs
184 | batslib_print_kv_single_or_multi() {
185 | local -ir width="$1"
186 | shift
187 | local -a pairs=("$@")
188 |
189 | local -a values=()
190 | local -i i
191 | for ((i = 1; i < ${#pairs[@]}; i += 2)); do
192 | values+=("${pairs[$i]}")
193 | done
194 |
195 | if batslib_is_single_line "${values[@]}"; then
196 | batslib_print_kv_single "$width" "${pairs[@]}"
197 | else
198 | local -i i
199 | for ((i = 1; i < ${#pairs[@]}; i += 2)); do
200 | pairs[$i]="$(batslib_prefix < <(printf '%s' "${pairs[$i]}"))"
201 | done
202 | batslib_print_kv_multi "${pairs[@]}"
203 | fi
204 | }
205 |
206 | # Prefix each line read from the standard input with the given string.
207 | #
208 | # Globals:
209 | # none
210 | # Arguments:
211 | # $1 - [= ] prefix string
212 | # Returns:
213 | # none
214 | # Inputs:
215 | # STDIN - lines
216 | # Outputs:
217 | # STDOUT - prefixed lines
218 | batslib_prefix() {
219 | local -r prefix="${1:- }"
220 | local line
221 | while IFS='' read -r line || [[ -n $line ]]; do
222 | printf '%s%s\n' "$prefix" "$line"
223 | done
224 | }
225 |
226 | # Mark select lines of the text read from the standard input by
227 | # overwriting their beginning with the given string.
228 | #
229 | # Usually the input is indented by a few spaces using `batslib_prefix'
230 | # first.
231 | #
232 | # Globals:
233 | # none
234 | # Arguments:
235 | # $1 - marking string
236 | # $@ - indices (zero-based) of lines to mark
237 | # Returns:
238 | # none
239 | # Inputs:
240 | # STDIN - lines
241 | # Outputs:
242 | # STDOUT - lines after marking
243 | batslib_mark() {
244 | local -r symbol="$1"
245 | shift
246 | # Sort line numbers.
247 | set -- $(sort -nu <<<"$(printf '%d\n' "$@")")
248 |
249 | local line
250 | local -i idx=0
251 | while IFS='' read -r line || [[ -n $line ]]; do
252 | if ((${1:--1} == idx)); then
253 | printf '%s\n' "${symbol}${line:${#symbol}}"
254 | shift
255 | else
256 | printf '%s\n' "$line"
257 | fi
258 | ((++idx))
259 | done
260 | }
261 |
262 | # Enclose the input text in header and footer lines.
263 | #
264 | # The header contains the given string as title. The output is preceded
265 | # and followed by an additional newline to make it stand out more.
266 | #
267 | # Globals:
268 | # none
269 | # Arguments:
270 | # $1 - title
271 | # Returns:
272 | # none
273 | # Inputs:
274 | # STDIN - text
275 | # Outputs:
276 | # STDOUT - decorated text
277 | batslib_decorate() {
278 | echo
279 | echo "-- $1 --"
280 | cat -
281 | echo '--'
282 | echo
283 | }
284 |
--------------------------------------------------------------------------------
/tests/common.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [[ "$(uname)" == "Darwin" ]]; then
4 | SHUNPO_TEST_DIR="/private/tmp/shunpo_test"
5 | else
6 | SHUNPO_TEST_DIR="/tmp/shunpo_test"
7 | fi
8 |
9 | function setup_env {
10 | HOME=${SHUNPO_TEST_DIR}/home
11 | mkdir -p $HOME
12 | XDG_DATA_HOME=${SHUNPO_TEST_DIR}/home/.local/share
13 | mkdir -p $XDG_DATA_HOME
14 | }
15 |
16 | function cleanup_env {
17 | rm $SHUNPO_TEST_DIR/home/.bashrc
18 | rm $SHUNPO_TEST_DIR/home/.bashrc$
19 |
20 | if [ -d "${SHUNPO_TEST_DIR}/home" ]; then
21 | rmdir ${SHUNPO_TEST_DIR}/home/
22 | fi
23 |
24 | if [ -d "${SHUNPO_TEST_DIR}" ]; then
25 | find ${SHUNPO_TEST_DIR} -type d -empty -delete
26 | rmdir $SHUNPO_TEST_DIR
27 | fi
28 | }
29 |
30 | make_directories() {
31 | # Make directory structure.
32 | local depth=4
33 | local width=3
34 | for i in $(seq 1 $depth); do
35 | if [[ $1 -eq 1 ]]; then
36 | run sb >/dev/null
37 | fi
38 | mkdir -p "$i"
39 | if [[ $i -ne 1 ]]; then
40 | for j in $(seq 1 $width); do
41 | mkdir -p "$i.$j"
42 | done
43 | fi
44 | cd "$i"
45 | done
46 | }
47 |
48 | get_num_bookmarks() {
49 | SHUNPO_BOOKMARKS_FILE="${XDG_DATA_HOME:-$HOME/.local/share}/shunpo/.shunpo_bookmarks"
50 | echo $(wc -l <$SHUNPO_BOOKMARKS_FILE | tr -d '[:space:]')
51 | }
52 |
--------------------------------------------------------------------------------
/tests/test_bookmarks.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | load common.sh
4 | load bats/assert.sh
5 | load bats/error.sh
6 | load bats/lang.sh
7 | load bats/output.sh
8 |
9 | setup() {
10 | echo "Setting Up Test."
11 | setup_env
12 | printf '\n' | ./install.sh
13 | working_dir=$(pwd)
14 | source ${SHUNPO_TEST_DIR}/home/.bashrc
15 | cd ${SHUNPO_TEST_DIR}
16 | }
17 |
18 | teardown() {
19 | echo "Shutting Down Test."
20 | cd "$working_dir"
21 | ./uninstall.sh
22 | }
23 |
24 | @test "Test Install." {
25 | [ -e "${SHUNPO_DIR}/shunpo_cmd" ] && assert_success
26 | [ "$(echo $SHUNPO_DIR)" = "${SHUNPO_DIR}" ] && assert_success
27 | [ -e "${SHUNPO_DIR}/functions.sh" ] && assert_success
28 | [ -e "${SHUNPO_DIR}/colors.sh" ] && assert_success
29 | [ -e "${SHUNPO_DIR}/add_bookmark.sh" ] && assert_success
30 | [ -e "${SHUNPO_DIR}/go_to_bookmark.sh" ] && assert_success
31 | [ -e "${SHUNPO_DIR}/remove_bookmark.sh" ] && assert_success
32 | [ -e "${SHUNPO_DIR}/list_bookmarks.sh" ] && assert_success
33 | [ -e "${SHUNPO_DIR}/clear_bookmarks.sh" ] && assert_success
34 | [ -e "${SHUNPO_DIR}/jump_to_parent.sh" ] && assert_success
35 | [ -e "${SHUNPO_DIR}/jump_to_child.sh" ] && assert_success
36 | run declare -F sb && assert_success
37 | run declare -F sg && assert_success
38 | run declare -F sr && assert_success
39 | run declare -F sl && assert_success
40 | run declare -F sc && assert_success
41 | run declare -F sj && assert_success
42 | run declare -F sd && assert_success
43 | }
44 |
45 | @test "Test Add Bookmark." {
46 | # Set up directory structure.
47 | make_directories 1
48 | assert_success
49 |
50 | # Check if bookmarks are created.
51 | num_bookmarks=$(get_num_bookmarks)
52 | assert_equal "$num_bookmarks" "4"
53 |
54 | # Check if bookmark entry is correct.
55 | bookmark2=$(sed -n 3p ${SHUNPO_DIR}/.shunpo_bookmarks)
56 | expected_bookmark2="${SHUNPO_TEST_DIR}/1/2"
57 | assert_equal "$bookmark2" "$expected_bookmark2"
58 | }
59 |
60 | @test "Test Go To Bookmark." {
61 | # Set up directory structure.
62 | make_directories 1
63 |
64 | # Check sg behavior.
65 | run sg 3 && assert_success
66 | sg 3 >/dev/null && echo $(pwd) #&& assert_success
67 | assert_equal $(pwd) "${SHUNPO_TEST_DIR}/1/2/3"
68 |
69 | run sg 2 && assert_success
70 | sg 2 >/dev/null && echo $(pwd) #&& assert_success
71 | assert_equal $(pwd) "${SHUNPO_TEST_DIR}/1/2"
72 |
73 | run sg 7 && assert_failure
74 | run sg "b" && assert_failure
75 | }
76 |
77 | @test "Test Remove Bookmark." {
78 | # Set up directory structure.
79 | make_directories 1
80 | assert [ -f ${SHUNPO_DIR}/.shunpo_bookmarks ]
81 |
82 | # Store the last bookmark.
83 | bookmark3=$(sed -n 4p ${SHUNPO_DIR}/.shunpo_bookmarks)
84 |
85 | # Check failure handling.
86 | run sr -1 >/dev/null && assert_failure
87 | run sr 9 >/dev/null && assert_failure
88 | run sr "c" >/dev/null && assert_failure
89 |
90 | # Remove bookmarks and check counts.
91 | run sr 1 && assert_success
92 | num_bookmarks=$(get_num_bookmarks)
93 | assert_equal "$num_bookmarks" "3"
94 |
95 | run sr 1 && assert_success
96 | num_bookmarks=$(get_num_bookmarks)
97 | assert_equal "$num_bookmarks" "2"
98 |
99 | # Check shifting.
100 | bookmark1=$(sed -n 2p ${SHUNPO_DIR}/.shunpo_bookmarks)
101 | assert_equal "$bookmark3" "$bookmark1"
102 |
103 | # Remove until file is removed.
104 | run sr 0 && assert_success
105 | run sr 0 && assert_success
106 | refute [ -f ${SHUNPO_DIR}/.shunpo_bookmarks ]
107 | }
108 |
109 | @test "Test Clear Bookmarks." {
110 | # Set up directory structure.
111 | make_directories 1
112 |
113 | # Check that the file exists
114 | assert [ -f ${SHUNPO_DIR}/.shunpo_bookmarks ]
115 |
116 | # Confirm that the file is removed after clearing.
117 | run sc && assert_success
118 | refute [ -f ${SHUNPO_DIR}/.shunpo_bookmarks ]
119 | }
120 |
--------------------------------------------------------------------------------
/tests/test_navigation.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | load common.sh
4 | load bats/assert.sh
5 | load bats/error.sh
6 | load bats/lang.sh
7 | load bats/output.sh
8 |
9 | setup() {
10 | echo "Setting Up Test."
11 | setup_env
12 | printf '\n' | ./install.sh
13 | working_dir=$(pwd)
14 | source ${SHUNPO_TEST_DIR}/home/.bashrc
15 | source ${SHUNPO_DIR}/scripts/functions.sh
16 | cd ${SHUNPO_TEST_DIR}
17 | }
18 |
19 | teardown() {
20 | echo "Shutting Down Test."
21 | cd "$working_dir"
22 | ./uninstall.sh
23 | }
24 |
25 | @test "Test Jump to Parent." {
26 | # Set up directory structure.
27 | make_directories 1
28 | cd ${SHUNPO_TEST_DIR}/1/2/3/4.2
29 |
30 | # Check expected success and failures.
31 | run sj 1 >/dev/null && assert_success
32 | run sj -3 >/dev/null && assert_failure
33 | run sj "b" >/dev/null && assert_failure
34 |
35 | # Check that post-jump directories are correct.
36 | sj 1 >/dev/null
37 | assert_equal $(pwd) "${SHUNPO_TEST_DIR}/1/2/3"
38 |
39 | sg 2 >/dev/null
40 | assert_equal $(pwd) "${SHUNPO_TEST_DIR}/1/2"
41 | }
42 |
--------------------------------------------------------------------------------
/uninstall.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | BASHRC="$HOME/.bashrc"
4 | source $BASHRC
5 |
6 | uninstall() {
7 | # Remove commands file.
8 | echo "Uninstalling..."
9 | SHUNPO_CMD="$SHUNPO_DIR/shunpo_cmd"
10 | if [ -f "$SHUNPO_CMD" ]; then
11 | rm "$SHUNPO_CMD"
12 | echo "Removed $SHUNPO_CMD"
13 | fi
14 |
15 | # Remove bookmarks file.
16 | SHUNPO_BOOKMARKS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/shunpo"
17 | SHUNPO_BOOKMARKS_FILE="$SHUNPO_BOOKMARKS_DIR/.shunpo_bookmarks"
18 | if [ -f $SHUNPO_BOOKMARKS_FILE ]; then
19 | rm $SHUNPO_BOOKMARKS_FILE
20 | echo "Removed $SHUNPO_BOOKMARKS_FILE"
21 | fi
22 |
23 | # Remove scripts and directories.
24 | if [ -z "$SHUNPO_DIR" ]; then
25 | echo "No Installation Found."
26 | exit 1
27 | else
28 | cd "${SHUNPO_DIR}"/scripts
29 | rm jump_to_parent.sh
30 | rm jump_to_child.sh
31 | rm add_bookmark.sh
32 | rm remove_bookmark.sh
33 | rm go_to_bookmark.sh
34 | rm list_bookmarks.sh
35 | rm clear_bookmarks.sh
36 | rm functions.sh
37 | rm colors.sh
38 | cd ..
39 | rmdir "${SHUNPO_DIR}"/scripts
40 | cd ..
41 | rmdir $SHUNPO_DIR
42 | echo "Removed $SHUNPO_DIR"
43 | unset SHUNPO_DIR
44 | fi
45 |
46 | # Remove SHUNPO_CMD source in bashrc.
47 | temp_file=$(mktemp)
48 | sed '/^source .*\/shunpo_cmd$/d' "$BASHRC" >"$temp_file"
49 | mv "$temp_file" "$BASHRC"
50 |
51 | # Remove SHUNPO_DIR export in bashrc.
52 | temp_file=$(mktemp)
53 | grep -v '^export SHUNPO_DIR=' "$BASHRC" >"$temp_file"
54 | mv "$temp_file" "$BASHRC"
55 | }
56 |
57 | uninstall
58 | echo "Done."
59 |
--------------------------------------------------------------------------------