Iterate over `dirs` in a bash script

I would like to iterate over `dirs`in a script, but the script will never show more than one (current) folder

#! /bin/bash

for i in `dirs` 
do
    echo ${i}
done
echo ++++++++++++++++++
for i in $( dirs -p )
do
    echo ${i}
done
echo ------------------
dirscontent=`dirs`
echo "dirs : $dirscontent"
echo ++++++++++++++++++

Prior to running the code I have cd/pushd a few folders. Running dirs in the terminal window will list more than one folder. If I type the code in the terminal window I will get the whole list of folders. How can I make it work in the script?

Welcome to the forum.

That's because the script is executed in a new shell, and its stack is empty. Try sourcing the script.

1 Like

Thanks, that explains it.

Now, I am stuck at filtering with associative arrays but I will make a separate topic if I cannot solve it.

function dirslist () {
    declare -A arr
    for i in ~ /usr /usr/local /usr ~ /etc
#    for i in $( dirs -p )
    do
    echo checking ${i}
    if [ ${arr[$i]+7} ] ; then
        echo "         ${i} exists, should not try add it again"
    else
        echo "         adding $i to arr"
        arr[ $i ]=1
    fi
    done
    for key in "${!arr[@]}"; do
    echo "Key: $key"
    done
}
 
1 Like

Please become accustomed to provide decent context info of your problem.

It is always helpful to carefully and detailedly phrase a request, and to support it with system info like OS and shell, related environment (variables, directory structures, options), preferred tools, adequate (representative) sample input and desired output data and the logics connecting the two including your own attempts at a solution, and, if existent, system (error) messages verbatim, to avoid ambiguities and keep people from guessing.

Where are you stuck? Running your given script yields

Key:  /home/myuser 
Key:  /etc 
Key:  /usr/local 
Key:  /usr 

on my system. Even the exotic ${arr[$i]+7} doesn't give an error message (but doesn't evaluate to something sensible either) .

I apologize for the non satisfactory context. I did not solve how to use associative arrays as a lookup table and used an ordinary array instead.

Context, operating system, bash version

More than 20 years ago, at the release of bash 2.0, I redefined the cd function and I have used my own version of cd ever since on various solaris, bsd and linux systems. Typing cd without any arguments would list the top 7 directories of DIRSTACK and typing cd with a number argument would change directory using the DIRSTACK. It was slightly annoying that the top of DIRSTACK did not necessarily contain unique directories but my cd function was good enough for usage and I never got around to refining it.

The other day I noticed that bash has evolved and now has associative arrays which I thought would solve the issue with the potentially repetitive directory listing.

The problem

I do not know how to check if a key already exists in an associative array. The exotic ${arr[$i]+7} was something I tried reading
wiki.nix-pro.com/view/BASH_associative_arrays#Check_if_key_exists

The basic algorithm I want to do is

foreach directory in DIRSTACK
   if the directory has not been presented to the user
       present the directory to the user
       remember (i.e. store) the directory

which in a sample test code would be

function dirlist () {
    declare -A arr
    for i in /usr /usr/local /usr "/foldername with spaces"  "/foldername with spaces"
    do
    echo checking ${i}
    if [[ ${arr[$i]} != 17 ]] ; then
        echo "        handling $i and adding it to arr"
        arr[ $i ]=17
    fi
    done
}

The above program yields

checking /usr
        handling /usr and adding it to arr
checking /usr/local
        handling /usr/local and adding it to arr
checking /usr
        handling /usr and adding it to arr
checking /foldername with spaces
        handling /foldername with spaces and adding it to arr
checking /foldername with spaces
        handling /foldername with spaces and adding it to arr

but what I expected (hoped) it to print was

checking /usr
        handling /usr and adding it to arr
checking /usr/local
        handling /usr/local and adding it to arr
checking /foldername with spaces
        handling /foldername with spaces and adding it to arr

The workaround

I rewrote my old cd function using a simple array for lookup instead of an associated array. The performance hit is negligible but it is annoying that I could not get the associated array to work. It was not without pain to implement since I haven't done any bash programming in 20 years. My new cd function seems to work all right though I haven't tested it extensively. I provide it here for context.

unset -f cd
function cd () {
    local -i maxnrDirs=7
    local -i nrDirs=0     # index of dirs as shown to user 
    local visited=()
    local -i founddir=0
    
    # if argument is a valid directory , change directory
    if [ -d "$1" ]; then
	pushd "$1" > /dev/null;
    else
	for ((i=1; i < ${#DIRSTACK[@]}; i++)); do
	    # keep track of already listed directories,
	    # Note, should be done with associative array
	    # this was on my TODO list for 20 years
	    local -i found=0
	    for ((j=0; j < ${#visited[@]}; j++)); do
		if [[ ${visited[$j]} == ${DIRSTACK[${i}]} ]]
		then
		    found=17
		    break
		fi
	    done
	    # handle directory option if not found
	    if [ $found == 0 ] ; then
		((nrDirs+=1))
		# if there is no argument to cd, list previous dirs
		if [ -z "$1" ]; then
		    founddir=17
		    echo " $nrDirs ${DIRSTACK[$i]}"; 
		    # if there is a number argument, check against DIRSTACK 
		else
		    if [ "$1" == ${nrDirs} ]; then
			if [ "${DIRSTACK[${i}]}" ]; then
			    if [ -d "`dirs -l +${i}`" ]; then
				pushd "`dirs -l +${i}`" > /dev/null ;
				founddir=${i};
			    fi;
			fi;
		    fi;
		fi
		local dir=${DIRSTACK[$i]}  # for quoting directory with spaces
		visited+=("$dir")          # visited+=("${DIRSTACK[$i]}") does not work
		if [ $nrDirs == $maxnrDirs ]; then
		    break;
		fi
	    fi
        done;
	if [ ${founddir} == 0 ]; then
            # Handle special characters, invent new ones ....?
            if [ "$1" == - ]; then
		pushd "`dirs -l +1`" > /dev/null;
	    elif [ "$1" == "..." ]; then
		if [ -d "../../" ]; then
		    pushd "../../../" > /dev/null ;
		fi
	    elif [ "$1" == "...." ]; then
		if [ -d "../../../" ]; then
		    pushd "../../../" > /dev/null ;
		fi
            elif [ ! -z "$1" ]; then
		# error message, TODO check \ ??
		echo ${1} not a valid directory;
            fi;
	fi;
    fi
}

complete -A directory cd
complete -A directory pushd

and here is a sample how it works

/> export PS1='\w> '
/> cd /
/> dirs -c
/> cd /usr/
/usr> cd local/
/usr/local> cd /etc/
/etc> cd cron.d/
/etc/cron.d> cd
 1 /etc
 2 /usr/local
 3 /usr
 4 /
/etc/cron.d> cd 2
/usr/local> cd -
/etc/cron.d> cd
 1 /usr/local
 2 /etc/cron.d
 3 /etc
 4 /usr
 5 /
/etc/cron.d> dirs -p
/etc/cron.d
/usr/local
/etc/cron.d
/etc
/usr/local
/usr
/

Wow, that's intricate and certainly beyond me on first sight. A few comments:

  • You don't need to check if an index is already represented in an associative array; just assign it. If it's aready there, it's overwritten, if not, added.
  • your cd without parameter naturally lends itself to bash 's select statement; see below (as a proof of concept; it just echo es the cd, no error checking etc.). Note the duplicate /usr entry:
echo ${DIRSTACK[@]}
/usr /etc/network /home/myuser /usr /mnt/9/plgr
declare -A TMP
for DN in ${DIRSTACK[@]}; do TMP[$DN]=1; done
select R in ${!TMP[@]}; do echo cd $R; break; done
1) /usr
2) /home/myuser
3) /etc/network
4) /mnt/9/plgr
#? 3
cd /etc/network