Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shell programming: Select random line from command output

Tags:

shell

macos

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"
like image 550
user4493605 Avatar asked Apr 30 '15 04:04

user4493605


3 Answers

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
like image 32
Rajesh N Avatar answered Sep 21 '22 05:09

Rajesh N


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.

like image 131
sorbet Avatar answered Sep 23 '22 05:09

sorbet


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.
like image 37
mklement0 Avatar answered Sep 19 '22 05:09

mklement0