5

I'm trying to create a simple Shell script which involves selecting a random directory from the current working directory, and navigating to it.

Can anyone illustrate how to list all directories, and from this list, randomly select one?

I'm trying to avoid listing all directories to a text file and simply selecting a random line from that file (which is simple enough).

My initial attempts included using the ls -d */ command to list only directories. This command worked when it was entered into the terminal, however returned the error:

ls: */: No such file or directory

when I tried to implement it into this script:

DIR_LIST=` ls -d */`

echo "$DIR_LIST"
user4493605
  • 391
  • 2
  • 18
  • 1
    You might specify OSX as a tag... – beroe Apr 30 '15 at 05:08
  • 1
    alternatives to `shuf` are listed at http://stackoverflow.com/questions/2153882/how-can-i-shuffle-the-lines-of-a-text-file-in-unix-command-line and http://stackoverflow.com/questions/2153882/how-can-i-shuffle-the-lines-of-a-text-file-in-unix-command-line – beroe Apr 30 '15 at 05:16

4 Answers4

4

find . -maxdepth 1 -type d ! -path . | shuf -n 1


No shuf version:

# To exclude hidden directories, use -name like so:
# find ... -name ".*"

# A version of find using only POSIX options:
# This might be slower for large directories (untested). Will need to be modified to list anything other than current directory.
# dirs="$(find . -type d ! -path "./*/*" ! -name ".")"

# Includes hidden directories.
dirs="$(find . -maxdepth 1 -type d ! -path .)"

numlines="$(printf "%s\n" "${dirs}" | wc -l)"
lineno="$((${RANDOM} % ${numlines} + 1))"
random_line="$(printf "%s\n" "${dirs}" | sed -n "${lineno}{p;q}")"

echo "Your selected directory: ${random_line}"

Edit: Improved code based on comments.

midrare
  • 2,371
  • 28
  • 48
  • 1
    Works well; it's worth pointing out that the `find` command returns _hidden_ subdirectories too, unlike glob `*` (at least by default); a number of optimizations are possible: `shuf | head -n 1` can be simplified to `shuf -n 1`, as demonstrated by Rajesh N's answer. Similarly, `head -n ${lineno} | tail -n 1` can be simplified to `sed -n "${lineno}p"` (or `sed -n "${lineno}{p;q;}` for max. efficiency). POSIX `echo`doesn't support any options, so given that this question is generically tagged `shell`, `printf '%s\n'` can be used in place of `echo -E` to make the non-`shuf` code POSIX-compliant. – mklement0 May 01 '15 at 14:04
  • Thanks for updating your answer and kudos for the POSIX-compliant `find` command, but note that a simple `find * -prune -type d` would do the trick in this case (solves both the issue of limiting traversal depth and excluding hidden dirs.). Of course, since that relies on the _shell_'s pathname-expansion mechanism anyway, you could just use `dirs=$(printf '%s\n' */)` - which is arguably the simplest (and equally POSIX-compliant) solution in this case. – mklement0 May 03 '15 at 04:47
3

The error message you're seeing indicates that the current directory happens to have no (non-hidden) subdirectories - the script's working directory is probably different from what you expect - cd to the desired dir. first.

Aside from that, however, it's better not to parse the output of ls;
using pathname expansion (globbing) directly is both simpler and more robust - see below.

Note: Originally, this answer contained only one solution, based on a bash array, which has been replaced with multi-line-string solutions, given the generic title of the question.


The question is tagged osx, which has two implications:

  • GNU utility shuf is NOT preinstalled.
    • However, you can install it (as gshuf) via Homebrew with brew install coreutils.
  • BSD utility jot IS preinstalled; while far from being the equivalent of shuf, it is capable of choosing a random number from a range of integers.

A jot-based solution:

dirs=$(printf '%s\n' */)  # collect subdir. names, each on its own line
randomLineNum=$(jot -r 1 1 $(wc -l <<<"$dirs")) # pick random line number
randomDir=$(sed -n "$randomLineNum{p;q;}" <<<"$dirs") # extract random dir. name
cd "$randomDir"    # use the randomly selected dir.
  • printf '%s\n' */ prints all directory names (with a terminating /); each on its own line.
    • Note that there is no good reason to use find in a simple case like this; the glob */ is sufficient to match all subdirectories.
  • jot -r 1 1 $(wc -l <<<"$dirs") returns a randomly chosen integer between 1 and the number of lines in $dirs (wc -l <<<"$dirs"), i.e., the number of subdirs.
  • sed -n '<lineNumber>{p;q;}' is a sed idiom that prints (p) only the line with the specified number and then quits (q) processing the file.

A POSIX-compliant solution:

Note: This can be handy if you cannot assume the presence of jot,shuf, or even bash.

dirs=$(printf '%s\n' */)
randomLineNum=$(awk -v count="$(printf '%s\n' "$dirs" | wc -l)" \
                  'BEGIN { srand(); print 1 + int(rand()* count) }')
randomDir=$(printf '%s\n' "$dirs" | sed -n "$randomLineNum{p;q;}")
cd "$randomDir"
  • printf '%s\n' "$dirs" | wc -l counts the number of lines in $dir
  • awk -v count=<lineCount> 'BEGIN { srand(); print 1 + int(rand()* count) }' uses awk to print a random number between 1 and :
    • srand() seeds the random generator, and rand() returns a random float >= 0 and < 1; by multiplying with the line count, converting to an integer and adding 1, a random number >= 1 <= line count is returned.

For the sake of completeness, let's look at shuf solutions:


Simplest, but inefficient solution using shuf:

printf '%s\n' */ | shuf -n 1
  • shuf -n 1 shuffles all input lines and then prints only the first of the shuffled lines.

This is inefficient, because even though only 1 random line is needed, shuf invariably reads all input lines at once into memory, and shuffles all of them instead of just picking 1 random one; with a small number of lines that probably won't matter, however.


Slightly more cumbersome, but more efficient shuf solution:

Note that this solution is similar to the jot-based one above.

dirs=$(printf '%s\n' */)
randomLineNum=$(shuf -n 1 -i 1-"$(wc -l <<<"$dirs")")
randomDir=$(sed -n "$randomLineNum{p;q;}" <<<"$dirs")
cd "$randomDir"
  • shuf -n 1 -i 1-"$(wc -l <<<"$dirs")" shuffles integers in the range between 1 and the count of lines in $dirs (wc -l <<<"$dirs"), and prints only one (the first) -n 1 of the shuffled numbers, effectively yielding a single, random line number.
  • Shuffling only the range of line numbers rather than the lines themselves will typically be more efficient, but note that a random permutation of all integers in the range is still built up in memory - unlike with jot, which simply picks a single integer in the range.
mklement0
  • 382,024
  • 64
  • 607
  • 775
1

Try this:

ls -d */ | shuf -n 1

shuf selects random directory from your output.

Your script will look like this:

DIR_LIST=`ls -d */`
echo "$DIR_LIST" | shuf -n 1
Rajesh N
  • 2,554
  • 1
  • 13
  • 17
  • ++1 for `shuf -n 1`; as noted, `shuf` is a _GNU_ utility that is typically only preinstalled on _Linux_ distros. It won't make much difference in many cases, but I suggest replacing `ls -d */` with `printf '%s\n' */` for both better performance and better robustness. – mklement0 May 01 '15 at 13:57
0

You might try

find . -maxdepth 1 -type d

instead of using ls. I'm not at a terminal right now so I cannot test. It might be maxdepth 0.

Joe Gibbs
  • 144
  • 1
  • 12
  • `-maxdepth 1` is correct, but you _also_ need `-mindepth 1` (or `! -path .`, as in @sorbet's answer) to prevent inclusion of `.` – mklement0 Apr 30 '15 at 12:52