├── .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 | Logo 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 | ![Powered by 🍵](https://img.shields.io/badge/Powered%20by-%F0%9F%8D%B5-blue?style=flat-square) 14 | [![Ko-fi](https://img.shields.io/badge/Ko--fi-Buy%20me%20Tea-ff5f5f?logo=kofi&style=flat-square)](https://ko-fi.com/egurapha) 15 | ![Built With Nix](https://img.shields.io/badge/Built%20with-Nix-5277C3?logo=nixos&logoColor=white&style=flat-square) 16 | ![Code Formatting](https://img.shields.io/github/actions/workflow/status/egurapha/Shunpo/code_formatting.yml?branch=main&label=Code%20Formatting&style=flat-square) 17 | ![Unit Tests](https://img.shields.io/github/actions/workflow/status/egurapha/Shunpo/unit_testing.yml?branch=main&label=Unit%20Tests&style=flat-square) 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 | Shunpo Demo 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 | --------------------------------------------------------------------------------