-2

I am writing a ksh function (that is placed in the .profile file) that will present a menu of subdirectories and permit the user to choose one into which to cd. Here is the code:

# Menu driven subdirectory descent.
d(){
# Only one command line argument accepted
[ "$1" = "--" ] && shift $# #   Trap for "ls --" feature
wd=`pwd`; arg="${1:-$wd}"
dirs="`/bin/ls -AF $arg  2>/dev/null | grep /$ | tr -d \"/\"`"
#       Set the names of the subdirectories to positional parameters
if [ "$dirs" ] ;then
        set $dirs
        if [ $# -eq 1 -a "$arg" = "$wd" ] ;then cd $arg/$1; return; fi # trap: it's obvious; do it
else echo "No subdirectories found" >&2; return 1
fi
#       Format and display the menu
if [ `basename "${arg}X"` = "${arg}X" ] ;then arg="$wd/$arg"; fi # Force absolute path if relitive
echo -e "\n\t\tSubdirectories relative to ${arg}: \n"
j=1; for i; do echo -e "$j\t$i"; j=`expr $j + 1`; done | pr -r -t -4 -e3 
echo -e "\n\t\tEnter the number of your choice -- \c "
#       Convert user-input to directory-name and cd to it
read choice; echo
dir=`eval "(echo $\{"$choice"\})"`  #       Magic here.
[ "$choice" -a "$choice" -ge 1 -a "$choice" -le "$#" ] && cd $arg/`eval echo "$dir"`
}

This function works reasonably well with the exception of directory names that contain space characters. If the directory name contains a space, the set command sets each space delimited element of the directory name (instead of the complete directory name) into a separate positional parameter; that is not useful here.

I have attempted to set the $IFS shell variable (which contains a space, tab, and newline by default) to a single newline character with:

IFS=`echo`        # echo outputs a trailing newline character by default

Which appears to accomplish what is intended as verified with:

echo -e "$IFS\c" | hexdump -c

But despite my best efforts (over the course of several days work) I have failed to set the entire directory names that contain spaces as values for positional parameters.

What am I missing?

Suggestions are hereby solicited and most welcome.

ADVAthanksNCE

Bob

  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackoverflow.com/rooms/84462/discussion-on-question-by-bob-bascom-setting-shell-positional-parameters-with-s). – George Stocker Jul 28 '15 at 13:01

3 Answers3

1

Short answer: You can't do that. Don't try. See the ParsingLs page for an understanding of why programmatic use of ls is inherently error-prone.


You can't get -F behavior without implementing it yourself in shell (which is indeed feasible), but the following is the correct way to put a list of subdirectories into the argument list:

set -- */

If you don't want to have a literal / on the end of each entry:

set -- */       # put list of subdirectories into "$@"
set -- "${@%/}" # strip trailing / off each

Even better, though: Use an array to avoid needing eval magic later.

dirs=( */ )
dirs=( "${dirs[@]%/}" )
printf '%s\n' "${dirs[$choice]}" # emit entry at position $choice

Let's tie this all together:

d() {
  destdir=$(
    FIGNORE= # ksh93 equivalent to bash shopt -s dotglob
    while :; do
      subdirs=( ~(N)*/ ) # ksh93 equivalent to subdirs=( */ ) with shopt -s nullglob
      (( ${#subdirs[@]} > 2 )) || break # . and .. are two entries

      for idx in "${!subdirs[@]}"; do
        printf '%d) %q\n' "$idx" "${subdirs[$idx]%/}" >&2
      done

      printf '\nSelect a subdirectory: ' >&2
      read -r choice
      if [[ $choice ]]; then
        cd -- "${subdirs[$choice]}" || break
      else
        break
      fi
    done
    printf '%s\n' "$PWD"
  )
  [[ $destdir ]] && cd -- "$destdir"
}
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Thank you very much for this technique. It works brilliantly! – Bob Bascom Jul 27 '15 at 21:29
  • With regard to parsing ls, here is what appears to be a rebuttal to the pointer you kindly provided: . Before accepting that this question has been answered, I'll post a working version that incorporates your kind suggestions. Right now, here is a copy of a new version of the *d* function that passes *shellcheck*: – Bob Bascom Jul 29 '15 at 16:08
  • [Unfortunately I'm unable to post that code due to the limitation on character count stackexchange imposes on comments.] – Bob Bascom Jul 29 '15 at 16:19
  • You might consider posting your own answer with the code in question. – Charles Duffy Jul 29 '15 at 16:20
  • Being that this is my first experience with stackexchange, I'm not sure how to do that. Ah, I see the box at the bottom of this page... – Bob Bascom Jul 29 '15 at 16:25
  • By the way -- another major bug with the proposed practices from http://unix.stackexchange.com/questions/128985/why-not-parse-ls%3E -- `?` is not the only glob character; attempting to expand a glob to replace `?`s with literally correct characters will also attempt to apply other glob expressions in the name -- square braces and the like. – Charles Duffy Jul 29 '15 at 16:27
  • @BobBascom, see complete solution amended to this answer. – Charles Duffy Jul 29 '15 at 16:49
  • (also, did you see how much code the person writing the question at http://unix.stackexchange.com/questions/128985/why-not-parse-ls%3E had to use to get something that was semi-reliable? Would you want to need to do that every time you had a directory listing to perform?) – Charles Duffy Jul 29 '15 at 16:55
0

Although still not working, this version does pass shellcheck albeit with one exception:

3  # Menu driven subdirectory descent.
4  function d{
5  # Only one command line argument accepted
6  [ "$1" = "--" ] && shift $#        # Trap for "ls --" feature
7  wd="$PWD"; arg="${1:-$wd}"
8  set -- "${@%/}"              #       Set the names of the subdirectories to positional parameters
9  if [ $# -eq 1 -a "$arg" = "$wd" ] ;then cd "$arg/$1" || exit 1; return; # trap: it's obvious; do it
10  else echo "No subdirectories found" >&2; return 1
11  fi
12  #       Format and display the menu
13  if [[ $(basename "${arg}X") = "${arg}X" ]] ;then arg="$wd/${arg}"; fi # Force absolute path if relitive
14  echo -e "\n\t\tSubdirectories relative to ${arg}: \n"
15  j=1; for i; do echo -e "$j\t$i"; j=(expr $j + 1); done | pr -r -t -4 -e3 
16  echo -e "\n\t\tEnter the number of your choice -- \c "
17  #       Convert user-input to directory-name and cd to it
18  read -r choice; echo
19  dir=(eval "(echo $\{\"$choice\"\})")        #       Magic here.
20  [ "$choice" -a "$choice" -ge 1 -a "$choice" -le "$#" ] && cd "${arg}"/"$(eval echo "${dir}")" || exit 1
                                                                                      ^SC2128 Expanding an array without an index only gives the first element.
21  }

Once I have incorporated your suggestions into the code, and made it functional, I'll post it here, and mark my question answered. Thank you for your kind assistance.

  • `function` is bad form, being not present in POSIX sh -- use `d() {` instead. – Charles Duffy Jul 29 '15 at 16:43
  • There are also a great many comments I gave directly on the question (now moved to chat) which haven't been taken into account in this code. – Charles Duffy Jul 29 '15 at 16:44
  • (I misunderstood earlier, by the way -- thought you actually had a working answer you wanted to post -- when suggesting the "add an answer" button). – Charles Duffy Jul 29 '15 at 16:50
  • I sincerely appreciate your example code answer. It seems to work as expected with a few exceptions. 1. It places a backslash before space characters in directory names. 2. The directory menu is displayed vertically. While this will accommodate long directory names (which is useful), it limits the number of directory names the menu of choices may contain without scrolling off screen. Your code is succinct and elegant, and I appreciate your sagacious assistance very much. incidentally, *shellcheck* finds on line 6: "Expanding an array without an index only gives the first element. – Bob Bascom Jul 29 '15 at 17:46
  • Oh, it also fails to display directory names that begin with a dot. – Bob Bascom Jul 29 '15 at 17:50
  • Yes -- expanding to the first element only was the intended behavior there; the alternative would be to enable the `nullglob` shell option to make empty globs expand to nothing at all, rather than expanding to a single array element containing only the glob expression itself. However, doing that would have side effects unless we were careful to turn it off on function exit. – Charles Duffy Jul 29 '15 at 17:54
  • With respect to directory names starting with a dot, that's what the `dotglob` option is for; looks like we might need to enable some non-default shell functions after all. – Charles Duffy Jul 29 '15 at 17:55
  • Moved the code in my answer into a subshell to allow state changes to be performed (such as enabling shell functions) without polluting other code. – Charles Duffy Jul 29 '15 at 18:01
  • When attempting to source your *d* function under *ksh* I receive this error: "ksh: .: syntax error: `(' unexpected". I am grateful for your help none the less. It is a superior starting place to begin further development of this function than my old code. Thank you. – Bob Bascom Jul 29 '15 at 18:02
  • Hmm. `shopt` is indeed a bashism, but that's the only error I can reproduce. – Charles Duffy Jul 29 '15 at 18:03
  • Looked up the relevant ksh equivalents. Function given in my answer now works for me when tested with ksh93. – Charles Duffy Jul 29 '15 at 18:11
  • No more errors. The only nit, is the single-quotes around directory names displayed in the menu that contain a space character. – Bob Bascom Jul 29 '15 at 18:17
  • That's a behavior of the `%q`, which emits strings in such a way that if you paste them into a shell, the shell will interpret them unambiguously. If you don't want such unambiguous interpretation (and want to run the risk of filenames with garbage in them corrupting your terminal configuration when printed), change the `%q` to `%s`. – Charles Duffy Jul 29 '15 at 18:19
  • 0) .root # d 0) . 1) .. 2) admin 3) .aptitude 4) bin 5) .cache 6) .config 7) .dbus 8) Desktop 9) .dillo 10) Downloads 11) .fltk 12) .foo 13) 'foo moo' 14) .gconf 15) .gnome2 16) .gstreamer-0.10 17) .gvfs 18) .history 19) indiecity 20) .local 21) Mail 22) .Mathematica 23) .netsurf 24) Product [list truncated to accommodate limited comment length] Select a subdirectory: 13 0) . 1) .. Select a subdirectory: 12 ksh[15]: cd: bad directory /root/foo moo # – Bob Bascom Jul 29 '15 at 18:22
  • Waitaminute. You're putting in `12` at a menu where the options are `0` and `1`? That should be no surprise when it fails. Just press enter to exit in the directory you're in at the time. – Charles Duffy Jul 29 '15 at 18:27
  • Eh. Edited to exit when only two entries exist. – Charles Duffy Jul 29 '15 at 18:30
  • I've got to run now. But I'll be back to continue as time permits. Many thanks for sharing your expertise with me. – Bob Bascom Jul 29 '15 at 18:34
0

I've used the code you kindly wrote as a basis for the d function below. It pretty much does what I'd like, with a few little issues:

  1. All subdirectory names that contain a SPACE character are surrounded by characters, but those that do not are not.

  2. All subdirectory names that contain a SINGLE QUOTE character have that character escaped with a BACKSLASH character.

Given that 1 and 2 above cause no issues, they are acceptable, but not ideal.

  1. After user input does the cd, the menu of subdirectory names is again looped through. This could be considered a feature, I suppose. I tried substituting a return for the brake commands in the sections of code following the cd commands, but was unsuccessful in overcoming the subsequent looped menu.

  2. The inclusion of "." and ".." at the head of the menu of subdirectories is not ideal, and actually serves no good purpose.

------------------- Code Begins ------------------------------

d() {
    if [ "$BASH" ] && [ "$BASH" != "/bin/sh" ]; then
       echo "$FUNCNAME: ksh only";return 1
    fi

    FIGNORE= # ksh93 equivalent to bash shopt -s dotglob


    if [ ${#} -gt 0 ] ;then             # Only one command line argument accepted
      cd -- "$1" && return 0
    fi

    if [ `ls -AF1|grep /|wc -l` -eq 1 ] ;then       # cd if only one subdirectory
      cd -- `ls -AF1|grep /` && return 0
    fi

  destdir=$(
    while :; do
      subdirs=( ~(N)*/ ) # ksh93 equivalent to subdirs=( */ ) with shopt -s nullglob
      (( ${#subdirs[@]} > 2 )) || break # . and .. are two entries

      echo -e "\n\t\tSubdirectories below ${PWD}: \n" >&2

      for idx in "${!subdirs[@]}"; do
        printf '%d) %q\n' "$idx" "${subdirs[$idx]%/}" >&2
      done

      printf '\nSelect a subdirectory: ' >&2
      read -r
      if [[ $REPLY ]]; then
        cd -- "${subdirs[$REPLY]}" || break     # Continue to loop through subdirectories after cding
      else
        break
      fi
    done
    printf '%s\n' "$PWD"
  )

--------------------------- Code Ends ------------------------------------

So, overall I'm very pleased, and consider myself very fortunate to have received the knowledgeable assistance of such an accomplished Unix wizard. I can't thank you enough.