*Patch* >From eed27ce56dfccef103d51379215c38a8438ec998 Mon Sep 17 00:00:00 2001 From: Jason Stein <jstein...@gmail.com> Date: Sun, 13 Jul 2025 11:08:24 -0700 Subject: [PATCH] Add dh (directory history) builtin
- Implement dh builtin for frequency-based directory navigation - Add comprehensive test suite with run-dh, dh.tests, and dh.right - Integrate with bash builtin system via Makefile.in updates - Support options: -c (clear), -i (show counts), -m N (limit), -w (write) - Support navigation: dh N (go to #N), dh N-M (go to #N then up M levels) - Support partial matching for directory names - Persistent history stored in ~/.bash_dirhistory - Frequency-based sorting with most used directories having lower numbers --- builtins/Makefile.in | 4 +- builtins/cd.def | 6 + builtins/common.h | 3 + builtins/dh.def | 807 +++++++++++++++++++++++++++++++++++++++++++ tests/dh.right | 20 ++ tests/dh.tests | 57 +++ tests/run-dh | 4 + 7 files changed, 899 insertions(+), 2 deletions(-) create mode 100644 builtins/dh.def create mode 100644 tests/dh.right create mode 100644 tests/dh.tests create mode 100644 tests/run-dh diff --git a/builtins/Makefile.in b/builtins/Makefile.in index 7e0ec3242..53779c249 100644 --- a/builtins/Makefile.in +++ b/builtins/Makefile.in @@ -141,7 +141,7 @@ RL_LIBSRC = $(topdir)/lib/readline DEFSRC = $(srcdir)/alias.def $(srcdir)/bind.def $(srcdir)/break.def \ $(srcdir)/builtin.def $(srcdir)/caller.def \ $(srcdir)/cd.def $(srcdir)/colon.def \ - $(srcdir)/command.def $(srcdir)/declare.def $(srcdir)/echo.def \ + $(srcdir)/command.def $(srcdir)/declare.def $(srcdir)/dh.def $(srcdir)/echo.def \ $(srcdir)/enable.def $(srcdir)/eval.def $(srcdir)/getopts.def \ $(srcdir)/exec.def $(srcdir)/exit.def $(srcdir)/fc.def \ $(srcdir)/fg_bg.def $(srcdir)/hash.def $(srcdir)/help.def \ @@ -159,7 +159,7 @@ STATIC_SOURCE = common.c evalstring.c evalfile.c getopt.c bashgetopt.c \ OFILES = builtins.o \ alias.o bind.o break.o builtin.o caller.o cd.o colon.o command.o \ - common.o declare.o echo.o enable.o eval.o evalfile.o \ + common.o declare.o dh.o echo.o enable.o eval.o evalfile.o \ evalstring.o exec.o exit.o fc.o fg_bg.o hash.o help.o history.o \ jobs.o kill.o let.o mapfile.o \ pushd.o read.o return.o set.o setattr.o shift.o source.o \ diff --git a/builtins/cd.def b/builtins/cd.def index 5b39cb52b..71e933e5c 100644 --- a/builtins/cd.def +++ b/builtins/cd.def @@ -618,6 +618,9 @@ change_to_directory (char *newdir, int nolinks, int xattr) else set_working_directory (tdir); + /* Add to directory history */ + add_to_dir_history (tdir); + free (tdir); return (1); } @@ -652,6 +655,9 @@ change_to_directory (char *newdir, int nolinks, int xattr) else free (t); + /* Add to directory history */ + add_to_dir_history (tdir); + r = 1; } else diff --git a/builtins/common.h b/builtins/common.h index a169f494c..95a441309 100644 --- a/builtins/common.h +++ b/builtins/common.h @@ -242,6 +242,9 @@ extern int builtin_unbind_variable (const char *); extern SHELL_VAR *builtin_find_indexed_array (char *, int); extern int builtin_arrayref_flags (WORD_DESC *, int); +/* Functions from dh.def */ +extern void add_to_dir_history (char *); + /* variables from evalfile.c */ extern int sourcelevel; diff --git a/builtins/dh.def b/builtins/dh.def new file mode 100644 index 000000000..ddc19c60e --- /dev/null +++ b/builtins/dh.def @@ -0,0 +1,807 @@ +This file is dh.def, from which is created dh.c. +It implements the builtin "dh" in Bash. + +Copyright (C) 2025 Free Software Foundation, Inc. + +This file is part of GNU Bash, the Bourne Again SHell. + +Bash is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Bash is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Bash. If not, see <http://www.gnu.org/licenses/>. + +$PRODUCES dh.c + +#include <config.h> + +#if defined (HAVE_UNISTD_H) +# include <unistd.h> +#endif + +#include "../bashansi.h" +#include "../bashintl.h" + +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> +#include "../shell.h" +#include "../general.h" +#include "../builtins.h" +#include "common.h" +#include "bashgetopt.h" + +/* Directory history entry structure */ +struct dir_entry { + char *directory; + int count; + struct dir_entry *next; +}; + +/* Global directory history list */ +static struct dir_entry *dir_history = NULL; + +/* Function declarations */ +void add_to_dir_history (char *dir); +static struct dir_entry *find_dir_entry (char *dir); +static void free_dir_history (void); +static void display_dir_history (int show_counts, int max_entries); +static char *get_dirhistory_file (void); +static void load_dir_history (void); +static void save_dir_history (void); +static int compare_dir_entries (const void *a, const void *b); +int change_to_dir_by_number (int dir_number); +static struct dir_entry **find_matching_dirs (char *partial_name, int *match_count); +static void display_matching_dirs (struct dir_entry **matches, int match_count); + +$BUILTIN dh +$FUNCTION dh_builtin +$SHORT_DOC dh [-ciw] [-m N] [N|partial_name] +Directory history navigation. + +Display the directories that have been visited using the cd command, +ordered by frequency of access. Directories are numbered with the most +frequently accessed directory having index 1 (displayed last in the list). + +Options: + -c clear the directory history + -i show visit counts in parentheses + -m N limit display to N most recent directories + -w write directory history to file + +Usage: + dh display directory list + dh -i display directory list with visit counts + dh -m 10 display only the 10 most recent directories + dh -w save directory history to file + dh N change to directory number N from the list + dh N-M change to directory number N, then go up M levels + dh partial_name change to directory matching partial_name, or show matches + +Examples: + dh 5 change to the 5th directory in history + dh 5-2 change to the 5th directory, then go up 2 parent directories + dh -m 5 -i show only 5 most recent directories with visit counts + dh proj change to directory containing 'proj' in its basename + dh bash change to directory containing 'bash', or show all matches + +Exit Status: +Returns success unless an invalid option is given. +$END + +/* Add a directory to the history or increment its count */ +void +add_to_dir_history (char *dir) +{ + struct dir_entry *entry; + char *canonical_dir; + + if (dir == NULL || *dir == '\0') + return; + + /* Load history on first use */ + static int history_loaded = 0; + if (!history_loaded) + { + load_dir_history (); + history_loaded = 1; + } + + /* Canonicalize the directory path to avoid duplicates */ + canonical_dir = sh_realpath (dir, NULL); + if (canonical_dir == NULL) + { + /* If realpath fails, try sh_canonpath with proper flags */ + canonical_dir = sh_canonpath (dir, PATH_CHECKDOTDOT|PATH_CHECKEXISTS); + if (canonical_dir == NULL) + canonical_dir = savestring (dir); + } + + /* Try to find existing entry */ + entry = find_dir_entry (canonical_dir); + if (entry) + { + entry->count++; + free (canonical_dir); + save_dir_history (); /* Save after updating count */ + return; + } + + /* Create new entry */ + entry = (struct dir_entry *)xmalloc (sizeof (struct dir_entry)); + entry->directory = canonical_dir; /* Use canonical path */ + entry->count = 1; + entry->next = dir_history; + dir_history = entry; + + save_dir_history (); /* Save after adding new directory */ +} + +/* Get the directory history file path */ +static char * +get_dirhistory_file (void) +{ + char *env_file, *home, *file; + + /* Check for DIRHISTFILE environment variable first */ + env_file = get_string_value ("DIRHISTFILE"); + if (env_file && *env_file) + return savestring (env_file); + + /* Default to ~/.bash_dirhistory in home directory */ + home = get_string_value ("HOME"); + if (home && *home) + { + file = (char *)xmalloc (strlen (home) + 20); + sprintf (file, "%s/.bash_dirhistory", home); + return file; + } + + /* Fallback to current directory */ + return savestring (".bash_dirhistory"); +} + +/* Load directory history from file */ +static void +load_dir_history (void) +{ + char *filename, *line; + FILE *file; + int count; + struct dir_entry *entry; + + filename = get_dirhistory_file (); + if (!filename) + return; + + file = fopen (filename, "r"); + free (filename); + + if (!file) + return; /* File doesn't exist or can't be read */ + + /* Read entries in format: count:directory */ + line = (char *)xmalloc (1024); + while (fgets (line, 1024, file)) + { + char *colon, *dir; + + /* Remove trailing newline */ + if (line[strlen(line) - 1] == '\n') + line[strlen(line) - 1] = '\0'; + + /* Parse count:directory format */ + colon = strchr (line, ':'); + if (!colon) + continue; + + *colon = '\0'; + count = atoi (line); + dir = colon + 1; + + if (count > 0 && *dir != '\0') + { + char *canonical_dir; + struct dir_entry *existing_entry; + + /* Canonicalize the directory path to match what add_to_dir_history does */ + canonical_dir = sh_realpath (dir, NULL); + if (canonical_dir == NULL) + { + /* If realpath fails, try sh_canonpath with proper flags */ + canonical_dir = sh_canonpath (dir, PATH_CHECKDOTDOT|PATH_CHECKEXISTS); + if (canonical_dir == NULL) + canonical_dir = savestring (dir); + } + + /* Check if we already have this canonical path */ + existing_entry = find_dir_entry (canonical_dir); + if (existing_entry) + { + /* Merge counts - use the higher count */ + if (count > existing_entry->count) + existing_entry->count = count; + free (canonical_dir); + } + else + { + /* Create new entry with canonical path */ + entry = (struct dir_entry *)xmalloc (sizeof (struct dir_entry)); + entry->directory = canonical_dir; + entry->count = count; + entry->next = dir_history; + dir_history = entry; + } + } + } + + free (line); + fclose (file); +} + +/* Save directory history to file */ +static void +save_dir_history (void) +{ + char *filename; + FILE *file; + struct dir_entry *entry; + + filename = get_dirhistory_file (); + if (!filename) + return; + + file = fopen (filename, "w"); + if (!file) + { + builtin_error (_("cannot write to %s: %s"), filename, strerror (errno)); + free (filename); + return; + } + + /* Write entries in format: count:directory */ + for (entry = dir_history; entry; entry = entry->next) + { + fprintf (file, "%d:%s\n", entry->count, entry->directory); + } + + fclose (file); + free (filename); +} + +/* Find a directory entry in the history */ +static struct dir_entry * +find_dir_entry (char *dir) +{ + struct dir_entry *entry; + + for (entry = dir_history; entry; entry = entry->next) + { + if (STREQ (entry->directory, dir)) + return entry; + } + return NULL; +} + +/* Free all directory history entries */ +static void +free_dir_history (void) +{ + struct dir_entry *entry, *next; + + for (entry = dir_history; entry; entry = next) + { + next = entry->next; + free (entry->directory); + free (entry); + } + dir_history = NULL; + + /* Also save to file when clearing */ + save_dir_history (); +} + +/* Comparison function for qsort - sort by count (ascending for display, but index numbers are reversed) */ +static int +compare_dir_entries (const void *a, const void *b) +{ + struct dir_entry * const *entry_a = (struct dir_entry * const *)a; + struct dir_entry * const *entry_b = (struct dir_entry * const *)b; + + return (*entry_a)->count - (*entry_b)->count; +} + +/* Display the directory history sorted by count */ +static void +display_dir_history (int show_counts, int max_entries) +{ + struct dir_entry *entry; + struct dir_entry **entries; + int count, i, display_count, start_index; + + /* Load history on first use */ + static int history_loaded = 0; + if (!history_loaded) + { + load_dir_history (); + history_loaded = 1; + } + + /* Count entries */ + count = 0; + for (entry = dir_history; entry; entry = entry->next) + count++; + + if (count == 0) + { + printf (_("No directories in history.\n")); + return; + } + + /* Create array for sorting */ + entries = (struct dir_entry **)xmalloc (count * sizeof (struct dir_entry *)); + + i = 0; + for (entry = dir_history; entry; entry = entry->next) + entries[i++] = entry; + + /* Sort by count (ascending - least frequent first, but index numbers will be reversed) */ + qsort (entries, count, sizeof (struct dir_entry *), compare_dir_entries); + + /* Determine how many entries to display and starting index */ + if (max_entries > 0 && max_entries < count) + { + display_count = max_entries; + start_index = count - max_entries; /* Show the most recent (highest count) entries */ + } + else + { + display_count = count; + start_index = 0; + } + + /* Display sorted entries with reversed index numbers (most frequent = index 1, displayed last) */ + for (i = start_index; i < start_index + display_count; i++) + { + if (show_counts) + printf ("%5d %s (%d)\n", count - i, entries[i]->directory, entries[i]->count); + else + printf ("%5d %s\n", count - i, entries[i]->directory); + } + + free (entries); +} + +/* Change to a directory by its number in the history list, optionally going up levels */ +int +change_to_dir_by_number_with_levels (int dir_number, int levels_up) +{ + struct dir_entry *entry; + struct dir_entry **entries; + int count, i; + char *target_dir, *modified_dir; + char *path_copy, *current_pos; + + /* Load history on first use */ + static int history_loaded = 0; + if (!history_loaded) + { + load_dir_history (); + history_loaded = 1; + } + + /* Count entries */ + count = 0; + for (entry = dir_history; entry; entry = entry->next) + count++; + + if (count == 0) + { + builtin_error (_("no directories in history")); + return (EXECUTION_FAILURE); + } + + if (dir_number < 1 || dir_number > count) + { + builtin_error (_("directory number %d out of range (1-%d)"), dir_number, count); + return (EXECUTION_FAILURE); + } + + /* Create array for sorting (same order as display) */ + entries = (struct dir_entry **)xmalloc (count * sizeof (struct dir_entry *)); + + i = 0; + for (entry = dir_history; entry; entry = entry->next) + entries[i++] = entry; + + /* Sort by count (ascending - least frequent first, but index numbers will be reversed) */ + qsort (entries, count, sizeof (struct dir_entry *), compare_dir_entries); + + /* Get the target directory using reversed indexing (1 = most frequent, at end of sorted array) */ + target_dir = entries[count - dir_number]->directory; + + /* If levels_up is 0, use the directory as-is */ + if (levels_up == 0) + { + modified_dir = target_dir; + } + else + { + /* Create a copy of the path to modify */ + path_copy = savestring (target_dir); + modified_dir = path_copy; + + /* Remove trailing slashes except for root */ + int len = strlen (path_copy); + while (len > 1 && path_copy[len - 1] == '/') + { + path_copy[len - 1] = '\0'; + len--; + } + + /* Go up the specified number of levels */ + for (i = 0; i < levels_up && strlen (path_copy) > 1; i++) + { + current_pos = strrchr (path_copy, '/'); + if (current_pos && current_pos != path_copy) + *current_pos = '\0'; + else if (current_pos == path_copy) + { + /* We're at root level, can't go higher */ + path_copy[1] = '\0'; /* Keep just "/" */ + break; + } + } + + modified_dir = path_copy; + } + + /* Print the directory we're changing to */ + printf ("%s\n", modified_dir); + + /* Change to the directory and update bash's internal state */ + if (chdir (modified_dir) == 0) + { + char *old_pwd; + + /* Get current PWD for OLDPWD */ + old_pwd = get_string_value ("PWD"); + if (old_pwd) + bind_variable ("OLDPWD", old_pwd, 0); + + /* Update shell's working directory and environment variables */ + set_working_directory (modified_dir); + + /* Update PWD environment variable */ + bind_variable ("PWD", modified_dir, 0); + + free (entries); + if (levels_up > 0) + free (path_copy); + return (EXECUTION_SUCCESS); + } + else + { + builtin_error (_("cannot change to directory '%s': %s"), modified_dir, strerror (errno)); + free (entries); + if (levels_up > 0) + free (path_copy); + return (EXECUTION_FAILURE); + } +} + +/* Parse directory number with optional levels up (N-M format) + Returns 1 if valid format, 0 if invalid + Sets dir_number and levels_up accordingly */ +static int +parse_dir_with_levels (char *word, int *dir_number, int *levels_up) +{ + char *dash_pos, *endptr; + long num1, num2; + + *levels_up = 0; /* Default to no levels up */ + + /* Look for dash */ + dash_pos = strchr (word, '-'); + if (dash_pos == NULL) + { + /* No dash, just a simple number */ + if (!all_digits (word)) + return 0; + + *dir_number = atoi (word); + return (*dir_number > 0) ? 1 : 0; + } + + /* Found dash, parse both numbers */ + *dash_pos = '\0'; /* Temporarily split the string */ + + /* Check first part (directory number) */ + if (!all_digits (word)) + { + *dash_pos = '-'; /* Restore the string */ + return 0; + } + + num1 = strtol (word, &endptr, 10); + if (*endptr != '\0' || num1 <= 0) + { + *dash_pos = '-'; /* Restore the string */ + return 0; + } + + /* Check second part (levels up) */ + if (!all_digits (dash_pos + 1)) + { + *dash_pos = '-'; /* Restore the string */ + return 0; + } + + num2 = strtol (dash_pos + 1, &endptr, 10); + if (*endptr != '\0' || num2 < 0) + { + *dash_pos = '-'; /* Restore the string */ + return 0; + } + + *dash_pos = '-'; /* Restore the string */ + + *dir_number = (int)num1; + *levels_up = (int)num2; + + return 1; +} + +/* Change to a directory by its number in the history list */ +int +change_to_dir_by_number (int dir_number) +{ + return change_to_dir_by_number_with_levels (dir_number, 0); +} + +/* The dh builtin */ +int +dh_builtin (WORD_LIST *list) +{ + int opt, clear_history, show_counts, write_history, max_entries; + char *word; + int dir_number, levels_up; + + clear_history = 0; + show_counts = 0; + write_history = 0; + max_entries = 0; /* 0 means show all entries */ + + /* Check for numeric argument first (before parsing options) */ + if (list && list->word && list->word->word) + { + word = list->word->word; + /* Check if it's a directory number with optional levels (N or N-M format) */ + if (parse_dir_with_levels (word, &dir_number, &levels_up)) + { + return (change_to_dir_by_number_with_levels (dir_number, levels_up)); + } + } + + reset_internal_getopt (); + while ((opt = internal_getopt (list, "cim:w")) != -1) + { + switch (opt) + { + case 'c': + clear_history = 1; + break; + case 'i': + show_counts = 1; + break; + case 'm': + max_entries = atoi (list_optarg); + if (max_entries <= 0) + { + builtin_error (_("invalid max entries: %s"), list_optarg); + return (EX_USAGE); + } + break; + case 'w': + write_history = 1; + break; + CASE_HELPOPT; + default: + builtin_usage (); + return (EX_USAGE); + } + } + list = loptend; + + /* Check for numeric argument or partial directory name after options */ + if (list && list->word && list->word->word) + { + word = list->word->word; + if (parse_dir_with_levels (word, &dir_number, &levels_up)) + { + return (change_to_dir_by_number_with_levels (dir_number, levels_up)); + } + else + { + /* Try partial directory matching */ + struct dir_entry **matches; + int match_count; + + matches = find_matching_dirs (word, &match_count); + + if (match_count == 0) + { + builtin_error (_("no directories match '%s'"), word); + return (EXECUTION_FAILURE); + } + else if (match_count == 1) + { + /* Single match - change to that directory */ + printf ("%s\n", matches[0]->directory); + if (chdir (matches[0]->directory) == 0) + { + char *old_pwd; + + /* Get current PWD for OLDPWD */ + old_pwd = get_string_value ("PWD"); + if (old_pwd) + bind_variable ("OLDPWD", old_pwd, 0); + + /* Update shell's working directory and environment variables */ + set_working_directory (matches[0]->directory); + + /* Update PWD environment variable */ + bind_variable ("PWD", matches[0]->directory, 0); + + free (matches); + return (EXECUTION_SUCCESS); + } + else + { + builtin_error (_("cannot change to directory '%s': %s"), + matches[0]->directory, strerror (errno)); + free (matches); + return (EXECUTION_FAILURE); + } + } + else + { + /* Multiple matches - display them */ + display_matching_dirs (matches, match_count); + free (matches); + return (EXECUTION_SUCCESS); + } + } + } + + if (clear_history) + { + free_dir_history (); + return (EXECUTION_SUCCESS); + } + + if (write_history) + { + save_dir_history (); + return (EXECUTION_SUCCESS); + } + + display_dir_history (show_counts, max_entries); + return (EXECUTION_SUCCESS); +} + +/* Search for directories matching a partial name */ +static struct dir_entry ** +find_matching_dirs (char *partial_name, int *match_count) +{ + struct dir_entry *entry; + struct dir_entry **matches; + char *basename_dir, *basename_partial; + int allocated_matches = 10; + int count = 0; + + if (!partial_name || !*partial_name) + { + *match_count = 0; + return NULL; + } + + /* Load history if not already loaded */ + static int history_loaded = 0; + if (!history_loaded) + { + load_dir_history (); + history_loaded = 1; + } + + matches = (struct dir_entry **)xmalloc (allocated_matches * sizeof (struct dir_entry *)); + basename_partial = strrchr (partial_name, '/'); + if (basename_partial) + basename_partial++; + else + basename_partial = partial_name; + + /* Search through directory history */ + for (entry = dir_history; entry; entry = entry->next) + { + /* Get the basename of the directory */ + basename_dir = strrchr (entry->directory, '/'); + if (basename_dir) + basename_dir++; + else + basename_dir = entry->directory; + + /* Check if the basename contains the partial name OR if the full path contains it */ + if (strstr (basename_dir, basename_partial) != NULL || + strstr (entry->directory, basename_partial) != NULL) + { + /* Expand array if needed */ + if (count >= allocated_matches) + { + allocated_matches *= 2; + matches = (struct dir_entry **)xrealloc (matches, + allocated_matches * sizeof (struct dir_entry *)); + } + matches[count++] = entry; + } + } + + *match_count = count; + if (count == 0) + { + free (matches); + return NULL; + } + + return matches; +} + +/* Display matching directories with their numbers */ +static void +display_matching_dirs (struct dir_entry **matches, int match_count) +{ + struct dir_entry *entry; + struct dir_entry **sorted_entries; + int i, count, index; + + if (match_count == 0) + return; + + /* Sort all directories to assign consistent numbers */ + count = 0; + for (entry = dir_history; entry; entry = entry->next) + count++; + + sorted_entries = (struct dir_entry **)xmalloc (count * sizeof (struct dir_entry *)); + i = 0; + for (entry = dir_history; entry; entry = entry->next) + sorted_entries[i++] = entry; + + qsort (sorted_entries, count, sizeof (struct dir_entry *), compare_dir_entries); + + printf ("Multiple matches found:\n"); + + /* Display matches with their numbers */ + for (i = 0; i < match_count; i++) + { + /* Find the index of this match in the sorted list */ + for (index = 0; index < count; index++) + { + if (sorted_entries[index] == matches[i]) + { + printf ("%5d %s\n", count - index, matches[i]->directory); + break; + } + } + } + + free (sorted_entries); +} diff --git a/tests/dh.right b/tests/dh.right new file mode 100644 index 000000000..5ecd42105 --- /dev/null +++ b/tests/dh.right @@ -0,0 +1,20 @@ +Testing dh builtin basic functionality +Empty history test: +PASS: Empty history message +History display test: +PASS: History contains dir1 +PASS: History contains dir2 +Visit counts test: +PASS: Visit counts shown +Limited display test: +PASS: Limited display works +Clear history test: +PASS: History cleared +Error handling test: +PASS: Invalid max entries error +PASS: Out of range error +Help output test: +PASS: Help text available +Partial matching test: +PASS: Multiple matches detected +dh builtin tests completed diff --git a/tests/dh.tests b/tests/dh.tests new file mode 100644 index 000000000..572c56a40 --- /dev/null +++ b/tests/dh.tests @@ -0,0 +1,57 @@ +# tests for the dh builtin (directory history) + +# Test basic functionality +echo "Testing dh builtin basic functionality" + +# Start with clean history +dh -c + +# Test 1: Empty history +echo "Empty history test:" +dh | grep -q "No directories in history" && echo "PASS: Empty history message" || echo "FAIL: Empty history message" + +# Test 2: Build and display history +mkdir -p /tmp/dhtest$$/dir1 /tmp/dhtest$$/dir2 2>/dev/null +cd /tmp/dhtest$$/dir1 +cd /tmp/dhtest$$/dir2 +cd /tmp/dhtest$$/dir1 # increase frequency + +echo "History display test:" +dh | grep -q "dir1" && echo "PASS: History contains dir1" || echo "FAIL: History missing dir1" +dh | grep -q "dir2" && echo "PASS: History contains dir2" || echo "FAIL: History missing dir2" + +# Test 3: Visit counts +echo "Visit counts test:" +dh -i | grep -q "(2)" && echo "PASS: Visit counts shown" || echo "FAIL: Visit counts not shown" + +# Test 4: Limited display +echo "Limited display test:" +count=$(dh -m 1 | grep -v "directories in history" | wc -l) +test "$count" -eq 1 && echo "PASS: Limited display works" || echo "FAIL: Limited display failed" + +# Test 5: Clear history +echo "Clear history test:" +dh -c +dh | grep -q "No directories in history" && echo "PASS: History cleared" || echo "FAIL: History not cleared" + +# Test 6: Error handling +echo "Error handling test:" +dh -m 0 2>&1 | grep -q "invalid max entries" && echo "PASS: Invalid max entries error" || echo "FAIL: Missing error for invalid max" +# Build some history for the out of range test +cd /tmp/dhtest$$/dir1 +cd /tmp/dhtest$$/dir2 +dh 999 2>&1 | grep -q "out of range" && echo "PASS: Out of range error" || echo "FAIL: Missing out of range error" + +# Test 7: Help output +echo "Help output test:" +dh --help 2>&1 | grep -q "Directory history navigation" && echo "PASS: Help text available" || echo "FAIL: Help text missing" + +# Test 8: Partial matching +cd /tmp/dhtest$$/dir1 +cd /tmp/dhtest$$/dir2/subdir 2>/dev/null || mkdir -p /tmp/dhtest$$/dir2/subdir && cd /tmp/dhtest$$/dir2/subdir +echo "Partial matching test:" +dh dir 2>&1 | grep -q "Multiple matches" && echo "PASS: Multiple matches detected" || echo "FAIL: Multiple matches not detected" + +# Cleanup +rm -rf /tmp/dhtest$$ 2>/dev/null +echo "dh builtin tests completed" diff --git a/tests/run-dh b/tests/run-dh new file mode 100644 index 000000000..78b8dea98 --- /dev/null +++ b/tests/run-dh @@ -0,0 +1,4 @@ +#!/bin/sh + +${THIS_SH} ./dh.tests > ${BASH_TSTOUT} 2>&1 +diff ${BASH_TSTOUT} dh.right && rm -f ${BASH_TSTOUT} On Sun, Jul 13, 2025 at 1:19 PM jason stein <jstein...@gmail.com> wrote: > *Per Lawrence **Velázquez* > Sending a new email to bug-bash@gnu.org. Original was sent to > help-bash@gnu-org which Lawrence indicated was the incorrect mailing list > (Thanks Lawrence). > > Hey All, > I developed a new builtin and I was wondering what is the process for > getting it added to the official release (e.g. reviews, votes, etc). > > *Pull Request:* > https://github.com/jstein916/bash/pull/1 > > *What the command dh does:* > it adds a new command called "dh" which stands for directory history. > The new command is similar to history but deals exclusively with the > directories that have been cd to. It creates a .bash_dirhistory file > similar to history. > > *Help from bash:* > dh: dh [-ciw] [-m N] [N|partial_name] > Directory history navigation. > > Display the directories that have been visited using the cd command, > ordered by frequency of access. Directories are numbered with the most > frequently accessed directory having index 1 (displayed last in the > list). > > Options: > -c clear the directory history > -i show visit counts in parentheses > -m N limit display to N most recent directories > -w write directory history to file > > Usage: > dh display directory list > dh -i display directory list with visit > counts > dh -m 10 display only the 10 most recent > directories > dh -w save directory history to file > dh N change to directory number N from the > list > dh N-M change to directory number N, then go > up M levels > dh partial_name change to directory matching > partial_name, or show matches > > Examples: > dh 5 change to the 5th directory in history > dh 5-2 change to the 5th directory, then go > up 2 parent directories > dh -m 5 -i show only 5 most recent directories > with visit counts > dh proj change to directory containing 'proj' > in its basename > dh bash change to directory containing 'bash', > or show all matches > > Exit Status: > Returns success unless an invalid option is given. > > > *Examples:* > Jason@PC MSYS /c/Projects/bash > $ cd /tmp/test1 > > Jason@PC MSYS /tmp/test1 > $ cd /tmp/test2/ > > Jason@PC MSYS /tmp/test2 > $ cd /tmp/test3 > > Jason@PC MSYS /tmp/test3 > $ dh > 3 /tmp/test3 > 2 /tmp/test2 > 1 /tmp/test1 > > Jason@PC MSYS /tmp/test3 > $ dh 31 > bash: dh: directory number 31 out of range (1-3) > > Jason@PC MSYS /tmp/test3 > $ dh 1 > /tmp/test1 > > Jason@PC MSYS /tmp/test1 > $ dh test3 > /tmp/test3 > > Jason@PC MSYS /tmp/test3 > $ dh > 3 /tmp/test3 > 2 /tmp/test2 > 1 /tmp/test1 > > Jason@PC MSYS /tmp/test3 > $ dh 2-1 > /tmp > > Jason@PC MSYS /tmp > > > $ dh tmp > Multiple matches found: > 1 /tmp/test3 > 3 /tmp/test2 > 2 /tmp/test1 > > > Thanks > > Jason >