0

I'm writing quizzes for my students in a markdown language. One of the quizzes might look like this:

% QUESTION
Who played drums for The Beatles?

(X) Ringo
( ) John
( ) Paul
( ) George

% QUESTION
What is the first line of MOBY DICK?

(X) Call me Ishmael.
( ) foo
( ) bar
( ) spam
( ) eggs

I'd like to randomize all of these multiple choice options. So, I think I need a shell script that:

  1. Finds all blocks of consecutive lines that start with (X) or ( ).
  2. Shuffles each of these blocks of lines.

Is this possible? I know that shuf and sort -R will randomize the lines of any text but I'm not sure of how to go about isolating these blocks of options.

terdon
  • 234,489
  • 66
  • 447
  • 667
Brian Fitzpatrick
  • 2,755
  • 3
  • 23
  • 43

5 Answers5

5

Using AWK:

BEGIN {
    srand()
    answers[1] = ""
    delete answers[1]
}

function outputanswers(answers, len, i) {
    len = length(answers)
    while (length(answers) > 0) {
        i = int(rand() * len + 1)
        if (answers[i]) {
            print answers[i]
        }
        delete answers[i]
    }
}

/^$/ {
    outputanswers(answers)
    print
}

/^[^(]/

/^\(/ {
    answers[length(answers) + 1] = $0
}

END { outputanswers(answers) }

This works by accumulating answers in the answers array, and outputting its contents in a random order when necessary. Lines are considered to be answers if they start with an opening parenthesis (I’m hoping that’s a valid simplification of your specification).

Stephen Kitt
  • 411,918
  • 54
  • 1,065
  • 1,164
1

A Perl approach:

perl -00 -M"List::Util qw/shuffle/" -lpe 'if(/^\(/){$_=join "\n",shuffle split(/\n/); }' file

And the same thing as a commented script:

#!/usr/bin/env perl

## Import the shuffle function from the List::Util module.
## This is done by the -M in the one-liner .
use List::Util qw/shuffle/;
    
## Enable paragraph mode, where each input record is a paragraph.
## This is equivalent to -00 in the one-liner. 
$/ = "";

## set output record separator (done by -l when after -00)
$\ = "\n\n";

## Read each record of standard input into the special variable $_.
## -p in the one-liner adds a similar implicit loop around the code
## given to -e.
while (<>) {
  ## strip trailing newlines (done by -l in the one-liner)
  chomp;

  ## If this record starts with a parenthesis
  if(/^\(/){
    ## Split the record (here, the entire paragraph, the whole section
    ## until the next sequence of one or more empty lines) on newline
    ## characters and save in the array @lines. In the one-liner, I skipped 
    ## creating this temporary array and joined the split directly
    @lines = split(/\n/);
    ## Set the special variable $_ to hold the shuffled contents of
    ## the @lines array, now connected with newline characters again.
    $_ = join "\n",shuffle @lines
  }

  ## Print whatever is in the $_ variable. That's the additional thing
  ## -p does compared to -n.
  print
}

And, just for fun, here's a slightly shorter version:

perl -MList::Util=shuffle -00lpe'$_=join"\n",shuffle split/\n/ if/^\(/' file
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
terdon
  • 234,489
  • 66
  • 447
  • 667
1

perl:

perl -F'\n' -MList::Util=shuffle -pal -00e '$"=$\;
  $_ = "@{[/^\([X ]\)/ ? shuffle(@F) : @F]}"
     . ($", $,)[eof];
' file
  • invoke in paragraph mode -00 and autosplit -a the para on newline -F'\n', the fields being stored in zero indexed array @F
  • Load the List::Util module -M and from that import the shuffle function.
  • For a para that begins with (X) or ( ), we shuffle the fields whilst for the other paras we don't.

GNU sed

sed -ne '
  /^([X ])/!{p;d;}       ;# just print noninteresting lines
  :loop 
    H;$bshfl           # accumulate the interesting lines in hold space 
    n
  //bloop
  :shfl
  x;s/.//       ;# retrieve hold n strip leading newline 
  s/.*/printf %s\\n "&" | shuf/ep  ;# shuffle 
  z;x;/^([X ])/!s/^/\n/;D   ;# null hold n loop back for more 
' file

Output: from the current run

% QUESTION
Who played drums for The Beatles?

( ) John
( ) Georgen
( ) Paul
(X) Ringo

% QUESTION
What is the first line of MOBY DICK?

( ) eggsn
(X) Call me Ishmael.
( ) bar
( ) spam
( ) foo
guest_7
  • 5,698
  • 1
  • 6
  • 13
0

This perl one would be the literal translation of the subject of your question:

perl -MList::Util=shuffle -0777 -pe'
   s{(?:^\([X ]\).*\n)+}{
     join "", shuffle $& =~ /.*\n/g
   }gme' < file

It substitutes every sequence of one or more (+) strings matching ^\([X ]\).*\n with the lines (.*\n) of which ($&) suffled.

For an approach using GNU sort -R:

<file awk '/^\([X ]\)/{if(y)n++;x=1};{y=x;x=0;print NR-n"\t"$0}' |
  sort -k1,1n -k1R |
  cut -f2-

awk decorates the input as:

1       % QUESTION
2       Who played drums for The Beatles?
3
4       (X) Ringo
4       ( ) John
4       ( ) Paul
4       ( ) George
5
6       % QUESTION
7       What is the first line of MOBY DICK?
8
9       (X) Call me Ishmael.
9       ( ) foo
9       ( ) bar
9       ( ) spam
9       ( ) eggs

Then sort sorts on the first field first and resolves ties randomly.

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
0

Just pipe the lines you want shuffled to shuf and print the rest as-is:

$ awk '/^\(/{print | "shuf"; next} !NF{close("shuf")} 1' file
% QUESTION
Who played drums for The Beatles?

( ) John
( ) Paul
(X) Ringo
( ) George

% QUESTION
What is the first line of MOBY DICK?

(X) Call me Ishmael.
( ) foo
( ) spam
( ) eggs
( ) bar
Ed Morton
  • 28,789
  • 5
  • 20
  • 47