learn_bash_from_session_data 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/bin/learn-bash.js +328 -0
- package/package.json +23 -0
- package/scripts/__init__.py +34 -0
- package/scripts/analyzer.py +591 -0
- package/scripts/extractor.py +411 -0
- package/scripts/html_generator.py +2029 -0
- package/scripts/knowledge_base.py +1593 -0
- package/scripts/main.py +443 -0
- package/scripts/parser.py +623 -0
- package/scripts/quiz_generator.py +1080 -0
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Quiz Generator for Bash Learning System
|
|
4
|
+
|
|
5
|
+
Generates quizzes from analyzed session commands with 4 question types:
|
|
6
|
+
1. "What does this do?" - Multiple choice about command behavior
|
|
7
|
+
2. "Which flag?" - Identify correct flag for a task
|
|
8
|
+
3. "Build the command" - Arrange components to form command
|
|
9
|
+
4. "Spot the difference" - Explain difference between similar commands
|
|
10
|
+
|
|
11
|
+
Uses ONLY Python standard library.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Optional
|
|
17
|
+
import random
|
|
18
|
+
import re
|
|
19
|
+
import hashlib
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class QuizType(Enum):
|
|
23
|
+
"""Types of quiz questions."""
|
|
24
|
+
WHAT_DOES = "what_does_this_do"
|
|
25
|
+
WHICH_FLAG = "which_flag"
|
|
26
|
+
BUILD_COMMAND = "build_the_command"
|
|
27
|
+
SPOT_DIFFERENCE = "spot_the_difference"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class QuizOption:
|
|
32
|
+
"""A single quiz answer option."""
|
|
33
|
+
id: str
|
|
34
|
+
text: str
|
|
35
|
+
is_correct: bool
|
|
36
|
+
explanation: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class QuizQuestion:
|
|
41
|
+
"""A complete quiz question with options."""
|
|
42
|
+
id: str
|
|
43
|
+
quiz_type: QuizType
|
|
44
|
+
question_text: str
|
|
45
|
+
options: list[QuizOption]
|
|
46
|
+
correct_option_id: str
|
|
47
|
+
explanation: str
|
|
48
|
+
difficulty: int # 1-5
|
|
49
|
+
command_context: Optional[str] = None
|
|
50
|
+
tags: list[str] = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict:
|
|
53
|
+
"""Convert to dictionary for serialization."""
|
|
54
|
+
return {
|
|
55
|
+
"id": self.id,
|
|
56
|
+
"type": self.quiz_type.value,
|
|
57
|
+
"question": self.question_text,
|
|
58
|
+
"options": [
|
|
59
|
+
{
|
|
60
|
+
"id": opt.id,
|
|
61
|
+
"text": opt.text,
|
|
62
|
+
"is_correct": opt.is_correct,
|
|
63
|
+
"explanation": opt.explanation
|
|
64
|
+
}
|
|
65
|
+
for opt in self.options
|
|
66
|
+
],
|
|
67
|
+
"correct_answer": self.correct_option_id,
|
|
68
|
+
"explanation": self.explanation,
|
|
69
|
+
"difficulty": self.difficulty,
|
|
70
|
+
"command_context": self.command_context,
|
|
71
|
+
"tags": self.tags
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class Quiz:
|
|
77
|
+
"""A complete quiz containing multiple questions."""
|
|
78
|
+
id: str
|
|
79
|
+
title: str
|
|
80
|
+
description: str
|
|
81
|
+
questions: list[QuizQuestion]
|
|
82
|
+
total_points: int = 0
|
|
83
|
+
time_limit_seconds: Optional[int] = None
|
|
84
|
+
|
|
85
|
+
def __post_init__(self):
|
|
86
|
+
if self.total_points == 0:
|
|
87
|
+
self.total_points = len(self.questions)
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict:
|
|
90
|
+
"""Convert to dictionary for serialization."""
|
|
91
|
+
return {
|
|
92
|
+
"id": self.id,
|
|
93
|
+
"title": self.title,
|
|
94
|
+
"description": self.description,
|
|
95
|
+
"questions": [q.to_dict() for q in self.questions],
|
|
96
|
+
"total_points": self.total_points,
|
|
97
|
+
"time_limit_seconds": self.time_limit_seconds,
|
|
98
|
+
"question_count": len(self.questions)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# =============================================================================
|
|
103
|
+
# Flag Knowledge Base (embedded for standalone use)
|
|
104
|
+
# =============================================================================
|
|
105
|
+
|
|
106
|
+
FLAG_DATABASE: dict[str, dict[str, str]] = {
|
|
107
|
+
"ls": {
|
|
108
|
+
"-l": "Use long listing format with details",
|
|
109
|
+
"-a": "Show all files including hidden ones",
|
|
110
|
+
"-h": "Human-readable file sizes",
|
|
111
|
+
"-R": "List subdirectories recursively",
|
|
112
|
+
"-t": "Sort by modification time",
|
|
113
|
+
"-S": "Sort by file size",
|
|
114
|
+
"-r": "Reverse sort order",
|
|
115
|
+
"-1": "List one file per line",
|
|
116
|
+
"-d": "List directories themselves, not contents",
|
|
117
|
+
"-i": "Show inode numbers"
|
|
118
|
+
},
|
|
119
|
+
"grep": {
|
|
120
|
+
"-i": "Case-insensitive search",
|
|
121
|
+
"-r": "Search recursively in directories",
|
|
122
|
+
"-n": "Show line numbers",
|
|
123
|
+
"-v": "Invert match (show non-matching lines)",
|
|
124
|
+
"-c": "Count matching lines only",
|
|
125
|
+
"-l": "Show only filenames with matches",
|
|
126
|
+
"-w": "Match whole words only",
|
|
127
|
+
"-E": "Use extended regular expressions",
|
|
128
|
+
"-A": "Show lines after match",
|
|
129
|
+
"-B": "Show lines before match",
|
|
130
|
+
"-C": "Show context lines around match",
|
|
131
|
+
"-o": "Show only the matching part",
|
|
132
|
+
"-q": "Quiet mode, exit status only"
|
|
133
|
+
},
|
|
134
|
+
"find": {
|
|
135
|
+
"-name": "Search by filename pattern",
|
|
136
|
+
"-type": "Filter by file type (f=file, d=directory)",
|
|
137
|
+
"-size": "Filter by file size",
|
|
138
|
+
"-mtime": "Filter by modification time",
|
|
139
|
+
"-exec": "Execute command on found files",
|
|
140
|
+
"-delete": "Delete found files",
|
|
141
|
+
"-maxdepth": "Limit search depth",
|
|
142
|
+
"-mindepth": "Minimum search depth",
|
|
143
|
+
"-perm": "Filter by permissions",
|
|
144
|
+
"-user": "Filter by owner",
|
|
145
|
+
"-group": "Filter by group",
|
|
146
|
+
"-newer": "Find files newer than reference"
|
|
147
|
+
},
|
|
148
|
+
"chmod": {
|
|
149
|
+
"-R": "Change permissions recursively",
|
|
150
|
+
"-v": "Verbose output",
|
|
151
|
+
"-c": "Report only when changes made",
|
|
152
|
+
"-f": "Suppress error messages",
|
|
153
|
+
"--reference": "Use another file's permissions"
|
|
154
|
+
},
|
|
155
|
+
"cp": {
|
|
156
|
+
"-r": "Copy directories recursively",
|
|
157
|
+
"-i": "Prompt before overwriting",
|
|
158
|
+
"-f": "Force overwrite without prompting",
|
|
159
|
+
"-v": "Verbose output",
|
|
160
|
+
"-p": "Preserve file attributes",
|
|
161
|
+
"-a": "Archive mode (preserve all)",
|
|
162
|
+
"-n": "Do not overwrite existing files",
|
|
163
|
+
"-u": "Update only (copy if source is newer)",
|
|
164
|
+
"-l": "Create hard links instead of copying",
|
|
165
|
+
"-s": "Create symbolic links instead"
|
|
166
|
+
},
|
|
167
|
+
"mv": {
|
|
168
|
+
"-i": "Prompt before overwriting",
|
|
169
|
+
"-f": "Force overwrite without prompting",
|
|
170
|
+
"-v": "Verbose output",
|
|
171
|
+
"-n": "Do not overwrite existing files",
|
|
172
|
+
"-u": "Update only (move if source is newer)",
|
|
173
|
+
"-b": "Create backup of existing files"
|
|
174
|
+
},
|
|
175
|
+
"rm": {
|
|
176
|
+
"-r": "Remove directories recursively",
|
|
177
|
+
"-f": "Force removal without prompting",
|
|
178
|
+
"-i": "Prompt before each removal",
|
|
179
|
+
"-v": "Verbose output",
|
|
180
|
+
"-d": "Remove empty directories"
|
|
181
|
+
},
|
|
182
|
+
"mkdir": {
|
|
183
|
+
"-p": "Create parent directories as needed",
|
|
184
|
+
"-v": "Verbose output",
|
|
185
|
+
"-m": "Set permissions mode"
|
|
186
|
+
},
|
|
187
|
+
"cat": {
|
|
188
|
+
"-n": "Number all output lines",
|
|
189
|
+
"-b": "Number non-blank lines only",
|
|
190
|
+
"-s": "Squeeze multiple blank lines",
|
|
191
|
+
"-A": "Show all non-printing characters",
|
|
192
|
+
"-E": "Show $ at end of each line",
|
|
193
|
+
"-T": "Show tabs as ^I"
|
|
194
|
+
},
|
|
195
|
+
"tar": {
|
|
196
|
+
"-c": "Create a new archive",
|
|
197
|
+
"-x": "Extract files from archive",
|
|
198
|
+
"-v": "Verbose output",
|
|
199
|
+
"-f": "Specify archive filename",
|
|
200
|
+
"-z": "Compress with gzip",
|
|
201
|
+
"-j": "Compress with bzip2",
|
|
202
|
+
"-t": "List archive contents",
|
|
203
|
+
"-r": "Append files to archive",
|
|
204
|
+
"-u": "Update files in archive",
|
|
205
|
+
"--exclude": "Exclude files matching pattern"
|
|
206
|
+
},
|
|
207
|
+
"curl": {
|
|
208
|
+
"-o": "Write output to file",
|
|
209
|
+
"-O": "Save with remote filename",
|
|
210
|
+
"-L": "Follow redirects",
|
|
211
|
+
"-s": "Silent mode",
|
|
212
|
+
"-v": "Verbose output",
|
|
213
|
+
"-H": "Add custom header",
|
|
214
|
+
"-X": "Specify HTTP method",
|
|
215
|
+
"-d": "Send POST data",
|
|
216
|
+
"-I": "Fetch headers only",
|
|
217
|
+
"-k": "Allow insecure SSL connections",
|
|
218
|
+
"-u": "Specify username:password",
|
|
219
|
+
"-A": "Set User-Agent string"
|
|
220
|
+
},
|
|
221
|
+
"wget": {
|
|
222
|
+
"-O": "Write to specified file",
|
|
223
|
+
"-q": "Quiet mode",
|
|
224
|
+
"-v": "Verbose output",
|
|
225
|
+
"-c": "Continue partial download",
|
|
226
|
+
"-r": "Recursive download",
|
|
227
|
+
"-np": "Don't ascend to parent directory",
|
|
228
|
+
"-P": "Specify download directory",
|
|
229
|
+
"-N": "Only download newer files"
|
|
230
|
+
},
|
|
231
|
+
"git": {
|
|
232
|
+
"--amend": "Amend the previous commit",
|
|
233
|
+
"-m": "Specify commit message",
|
|
234
|
+
"-a": "Stage all modified files",
|
|
235
|
+
"-b": "Create and checkout new branch",
|
|
236
|
+
"-f": "Force operation",
|
|
237
|
+
"-v": "Verbose output",
|
|
238
|
+
"--hard": "Reset working directory and index",
|
|
239
|
+
"--soft": "Reset only HEAD",
|
|
240
|
+
"-u": "Set upstream tracking branch"
|
|
241
|
+
},
|
|
242
|
+
"ssh": {
|
|
243
|
+
"-p": "Specify port number",
|
|
244
|
+
"-i": "Specify identity file",
|
|
245
|
+
"-v": "Verbose mode",
|
|
246
|
+
"-X": "Enable X11 forwarding",
|
|
247
|
+
"-L": "Local port forwarding",
|
|
248
|
+
"-R": "Remote port forwarding",
|
|
249
|
+
"-N": "No remote command",
|
|
250
|
+
"-f": "Go to background"
|
|
251
|
+
},
|
|
252
|
+
"scp": {
|
|
253
|
+
"-r": "Copy directories recursively",
|
|
254
|
+
"-P": "Specify port number",
|
|
255
|
+
"-i": "Specify identity file",
|
|
256
|
+
"-v": "Verbose mode",
|
|
257
|
+
"-C": "Enable compression",
|
|
258
|
+
"-p": "Preserve file attributes"
|
|
259
|
+
},
|
|
260
|
+
"ps": {
|
|
261
|
+
"-e": "Show all processes",
|
|
262
|
+
"-f": "Full format listing",
|
|
263
|
+
"-u": "Show user-oriented format",
|
|
264
|
+
"-a": "Show processes for all users",
|
|
265
|
+
"-x": "Show processes without controlling terminal",
|
|
266
|
+
"aux": "Common combination for all processes"
|
|
267
|
+
},
|
|
268
|
+
"kill": {
|
|
269
|
+
"-9": "Force kill (SIGKILL)",
|
|
270
|
+
"-15": "Graceful termination (SIGTERM)",
|
|
271
|
+
"-l": "List all signal names",
|
|
272
|
+
"-s": "Specify signal by name"
|
|
273
|
+
},
|
|
274
|
+
"awk": {
|
|
275
|
+
"-F": "Specify field separator",
|
|
276
|
+
"-v": "Set variable",
|
|
277
|
+
"-f": "Read program from file"
|
|
278
|
+
},
|
|
279
|
+
"sed": {
|
|
280
|
+
"-i": "Edit files in place",
|
|
281
|
+
"-e": "Add script command",
|
|
282
|
+
"-n": "Suppress automatic printing",
|
|
283
|
+
"-r": "Use extended regular expressions",
|
|
284
|
+
"-E": "Use extended regular expressions (portable)"
|
|
285
|
+
},
|
|
286
|
+
"sort": {
|
|
287
|
+
"-n": "Sort numerically",
|
|
288
|
+
"-r": "Reverse sort order",
|
|
289
|
+
"-k": "Sort by specific field",
|
|
290
|
+
"-u": "Remove duplicates",
|
|
291
|
+
"-t": "Specify field delimiter",
|
|
292
|
+
"-h": "Human numeric sort"
|
|
293
|
+
},
|
|
294
|
+
"uniq": {
|
|
295
|
+
"-c": "Prefix lines with occurrence count",
|
|
296
|
+
"-d": "Only print duplicate lines",
|
|
297
|
+
"-u": "Only print unique lines",
|
|
298
|
+
"-i": "Ignore case differences"
|
|
299
|
+
},
|
|
300
|
+
"head": {
|
|
301
|
+
"-n": "Specify number of lines",
|
|
302
|
+
"-c": "Specify number of bytes"
|
|
303
|
+
},
|
|
304
|
+
"tail": {
|
|
305
|
+
"-n": "Specify number of lines",
|
|
306
|
+
"-f": "Follow file (watch for appends)",
|
|
307
|
+
"-c": "Specify number of bytes"
|
|
308
|
+
},
|
|
309
|
+
"wc": {
|
|
310
|
+
"-l": "Count lines",
|
|
311
|
+
"-w": "Count words",
|
|
312
|
+
"-c": "Count bytes",
|
|
313
|
+
"-m": "Count characters"
|
|
314
|
+
},
|
|
315
|
+
"diff": {
|
|
316
|
+
"-u": "Unified diff format",
|
|
317
|
+
"-r": "Recursively compare directories",
|
|
318
|
+
"-q": "Only report if files differ",
|
|
319
|
+
"-i": "Ignore case differences",
|
|
320
|
+
"-w": "Ignore whitespace",
|
|
321
|
+
"-B": "Ignore blank lines"
|
|
322
|
+
},
|
|
323
|
+
"du": {
|
|
324
|
+
"-h": "Human-readable sizes",
|
|
325
|
+
"-s": "Display only total for each argument",
|
|
326
|
+
"-a": "Include files, not just directories",
|
|
327
|
+
"-c": "Produce grand total",
|
|
328
|
+
"-d": "Max depth to display"
|
|
329
|
+
},
|
|
330
|
+
"df": {
|
|
331
|
+
"-h": "Human-readable sizes",
|
|
332
|
+
"-T": "Show filesystem type",
|
|
333
|
+
"-i": "Show inode information"
|
|
334
|
+
},
|
|
335
|
+
"chmod": {
|
|
336
|
+
"-R": "Change permissions recursively",
|
|
337
|
+
"-v": "Verbose output"
|
|
338
|
+
},
|
|
339
|
+
"chown": {
|
|
340
|
+
"-R": "Change ownership recursively",
|
|
341
|
+
"-v": "Verbose output",
|
|
342
|
+
"-h": "Affect symbolic links"
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Command categories for generating related distractors
|
|
347
|
+
COMMAND_CATEGORIES: dict[str, list[str]] = {
|
|
348
|
+
"file_listing": ["ls", "find", "locate", "tree", "stat"],
|
|
349
|
+
"file_manipulation": ["cp", "mv", "rm", "mkdir", "rmdir", "touch"],
|
|
350
|
+
"text_processing": ["grep", "sed", "awk", "cut", "sort", "uniq", "tr"],
|
|
351
|
+
"text_viewing": ["cat", "less", "more", "head", "tail", "wc"],
|
|
352
|
+
"archiving": ["tar", "zip", "unzip", "gzip", "gunzip", "bzip2"],
|
|
353
|
+
"networking": ["curl", "wget", "ssh", "scp", "rsync", "ping", "netstat"],
|
|
354
|
+
"process_management": ["ps", "kill", "top", "htop", "jobs", "bg", "fg"],
|
|
355
|
+
"version_control": ["git"],
|
|
356
|
+
"permissions": ["chmod", "chown", "chgrp"],
|
|
357
|
+
"disk_usage": ["du", "df", "mount", "umount"],
|
|
358
|
+
"comparison": ["diff", "cmp", "comm"]
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Common flag descriptions that can be swapped as distractors
|
|
362
|
+
COMMON_FLAG_DESCRIPTIONS: dict[str, list[str]] = {
|
|
363
|
+
"-v": ["Verbose output", "Version information", "Validate input"],
|
|
364
|
+
"-r": ["Recursive operation", "Reverse order", "Read-only mode"],
|
|
365
|
+
"-f": ["Force operation", "File input", "Format output"],
|
|
366
|
+
"-n": ["Numeric output", "No newline", "Number lines", "Dry run"],
|
|
367
|
+
"-i": ["Interactive mode", "Case-insensitive", "In-place edit"],
|
|
368
|
+
"-a": ["All items", "Append mode", "Archive mode"],
|
|
369
|
+
"-l": ["Long format", "List only", "Line mode"],
|
|
370
|
+
"-h": ["Human-readable", "Help message", "Show hidden"]
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _generate_id(content: str) -> str:
|
|
375
|
+
"""Generate a unique ID based on content."""
|
|
376
|
+
return hashlib.md5(content.encode()).hexdigest()[:8]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _get_command_category(cmd: str) -> Optional[str]:
|
|
380
|
+
"""Get the category of a command."""
|
|
381
|
+
for category, commands in COMMAND_CATEGORIES.items():
|
|
382
|
+
if cmd in commands:
|
|
383
|
+
return category
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _get_related_commands(cmd: str) -> list[str]:
|
|
388
|
+
"""Get commands in the same category."""
|
|
389
|
+
category = _get_command_category(cmd)
|
|
390
|
+
if category:
|
|
391
|
+
return [c for c in COMMAND_CATEGORIES[category] if c != cmd]
|
|
392
|
+
return []
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _parse_command(cmd_string: str) -> dict:
|
|
396
|
+
"""Parse a command string into components."""
|
|
397
|
+
parts = cmd_string.strip().split()
|
|
398
|
+
if not parts:
|
|
399
|
+
return {"base": "", "flags": [], "args": []}
|
|
400
|
+
|
|
401
|
+
base = parts[0]
|
|
402
|
+
flags = []
|
|
403
|
+
args = []
|
|
404
|
+
|
|
405
|
+
for part in parts[1:]:
|
|
406
|
+
if part.startswith("-"):
|
|
407
|
+
flags.append(part)
|
|
408
|
+
else:
|
|
409
|
+
args.append(part)
|
|
410
|
+
|
|
411
|
+
return {"base": base, "flags": flags, "args": args}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _get_flag_description(cmd: str, flag: str) -> Optional[str]:
|
|
415
|
+
"""Get description for a flag of a command."""
|
|
416
|
+
if cmd in FLAG_DATABASE:
|
|
417
|
+
# Handle flags like -la (combined short flags)
|
|
418
|
+
if flag in FLAG_DATABASE[cmd]:
|
|
419
|
+
return FLAG_DATABASE[cmd][flag]
|
|
420
|
+
# Try individual characters for combined flags
|
|
421
|
+
if len(flag) > 2 and flag.startswith("-") and not flag.startswith("--"):
|
|
422
|
+
for char in flag[1:]:
|
|
423
|
+
single_flag = f"-{char}"
|
|
424
|
+
if single_flag in FLAG_DATABASE[cmd]:
|
|
425
|
+
return FLAG_DATABASE[cmd][single_flag]
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _generate_distractor_flags(cmd: str, correct_flag: str, count: int = 3) -> list[str]:
|
|
430
|
+
"""Generate plausible distractor flags."""
|
|
431
|
+
distractors = []
|
|
432
|
+
|
|
433
|
+
# Get other flags from the same command
|
|
434
|
+
if cmd in FLAG_DATABASE:
|
|
435
|
+
other_flags = [f for f in FLAG_DATABASE[cmd].keys() if f != correct_flag]
|
|
436
|
+
random.shuffle(other_flags)
|
|
437
|
+
distractors.extend(other_flags[:count])
|
|
438
|
+
|
|
439
|
+
# If we need more, get common flags from other commands
|
|
440
|
+
if len(distractors) < count:
|
|
441
|
+
for other_cmd, flags in FLAG_DATABASE.items():
|
|
442
|
+
if other_cmd != cmd:
|
|
443
|
+
for flag in flags:
|
|
444
|
+
if flag not in distractors and flag != correct_flag:
|
|
445
|
+
distractors.append(flag)
|
|
446
|
+
if len(distractors) >= count:
|
|
447
|
+
break
|
|
448
|
+
if len(distractors) >= count:
|
|
449
|
+
break
|
|
450
|
+
|
|
451
|
+
return distractors[:count]
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _generate_distractor_descriptions(correct_desc: str, count: int = 3) -> list[str]:
|
|
455
|
+
"""Generate plausible wrong descriptions."""
|
|
456
|
+
distractors = []
|
|
457
|
+
|
|
458
|
+
# Collect all descriptions from FLAG_DATABASE
|
|
459
|
+
all_descriptions = []
|
|
460
|
+
for cmd_flags in FLAG_DATABASE.values():
|
|
461
|
+
all_descriptions.extend(cmd_flags.values())
|
|
462
|
+
|
|
463
|
+
# Remove duplicates and the correct answer
|
|
464
|
+
all_descriptions = list(set(all_descriptions))
|
|
465
|
+
all_descriptions = [d for d in all_descriptions if d.lower() != correct_desc.lower()]
|
|
466
|
+
|
|
467
|
+
random.shuffle(all_descriptions)
|
|
468
|
+
return all_descriptions[:count]
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def generate_what_does_quiz(
|
|
472
|
+
command: dict,
|
|
473
|
+
analyzed_data: Optional[dict] = None
|
|
474
|
+
) -> QuizQuestion:
|
|
475
|
+
"""
|
|
476
|
+
Generate a "What does this do?" quiz question.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
command: Dictionary with keys like 'command', 'description', 'complexity', etc.
|
|
480
|
+
analyzed_data: Optional additional analysis data
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
QuizQuestion instance
|
|
484
|
+
"""
|
|
485
|
+
cmd_string = command.get("command", "")
|
|
486
|
+
description = command.get("description", "")
|
|
487
|
+
complexity = command.get("complexity", 2)
|
|
488
|
+
|
|
489
|
+
parsed = _parse_command(cmd_string)
|
|
490
|
+
base_cmd = parsed["base"]
|
|
491
|
+
|
|
492
|
+
# Build the correct description
|
|
493
|
+
correct_desc = description
|
|
494
|
+
if not correct_desc:
|
|
495
|
+
# Generate from flags
|
|
496
|
+
flag_descs = []
|
|
497
|
+
for flag in parsed["flags"]:
|
|
498
|
+
fd = _get_flag_description(base_cmd, flag)
|
|
499
|
+
if fd:
|
|
500
|
+
flag_descs.append(fd)
|
|
501
|
+
correct_desc = f"Runs {base_cmd}"
|
|
502
|
+
if flag_descs:
|
|
503
|
+
correct_desc += " with: " + ", ".join(flag_descs)
|
|
504
|
+
|
|
505
|
+
# Generate distractors
|
|
506
|
+
distractor_descriptions = _generate_distractor_descriptions(correct_desc, 3)
|
|
507
|
+
|
|
508
|
+
# Make distractors more plausible by relating to the command
|
|
509
|
+
related_cmds = _get_related_commands(base_cmd)
|
|
510
|
+
if related_cmds and len(distractor_descriptions) < 3:
|
|
511
|
+
for rel_cmd in related_cmds[:3 - len(distractor_descriptions)]:
|
|
512
|
+
distractor_descriptions.append(f"Runs {rel_cmd} to process files")
|
|
513
|
+
|
|
514
|
+
# Ensure we have exactly 3 distractors
|
|
515
|
+
while len(distractor_descriptions) < 3:
|
|
516
|
+
distractor_descriptions.append(f"Performs an unrelated {base_cmd} operation")
|
|
517
|
+
|
|
518
|
+
# Create options (shuffle positions)
|
|
519
|
+
options = []
|
|
520
|
+
correct_id = "a"
|
|
521
|
+
|
|
522
|
+
all_answers = [correct_desc] + distractor_descriptions[:3]
|
|
523
|
+
random.shuffle(all_answers)
|
|
524
|
+
|
|
525
|
+
for i, answer in enumerate(all_answers):
|
|
526
|
+
opt_id = chr(ord('a') + i)
|
|
527
|
+
is_correct = (answer == correct_desc)
|
|
528
|
+
if is_correct:
|
|
529
|
+
correct_id = opt_id
|
|
530
|
+
options.append(QuizOption(
|
|
531
|
+
id=opt_id,
|
|
532
|
+
text=answer,
|
|
533
|
+
is_correct=is_correct,
|
|
534
|
+
explanation=f"{'Correct!' if is_correct else 'Incorrect.'} This command: {correct_desc}"
|
|
535
|
+
))
|
|
536
|
+
|
|
537
|
+
question_id = _generate_id(f"what_does_{cmd_string}")
|
|
538
|
+
|
|
539
|
+
return QuizQuestion(
|
|
540
|
+
id=question_id,
|
|
541
|
+
quiz_type=QuizType.WHAT_DOES,
|
|
542
|
+
question_text=f"What does this command do?\n\n```bash\n{cmd_string}\n```",
|
|
543
|
+
options=options,
|
|
544
|
+
correct_option_id=correct_id,
|
|
545
|
+
explanation=f"The command `{cmd_string}` {correct_desc.lower()}",
|
|
546
|
+
difficulty=min(complexity, 5),
|
|
547
|
+
command_context=cmd_string,
|
|
548
|
+
tags=[base_cmd, "what_does"]
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def generate_which_flag_quiz(
|
|
553
|
+
command: dict,
|
|
554
|
+
analyzed_data: Optional[dict] = None
|
|
555
|
+
) -> Optional[QuizQuestion]:
|
|
556
|
+
"""
|
|
557
|
+
Generate a "Which flag?" quiz question.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
command: Dictionary with command info
|
|
561
|
+
analyzed_data: Optional additional analysis data
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
QuizQuestion instance or None if not enough flag data
|
|
565
|
+
"""
|
|
566
|
+
cmd_string = command.get("command", "")
|
|
567
|
+
parsed = _parse_command(cmd_string)
|
|
568
|
+
base_cmd = parsed["base"]
|
|
569
|
+
|
|
570
|
+
if base_cmd not in FLAG_DATABASE or not parsed["flags"]:
|
|
571
|
+
return None
|
|
572
|
+
|
|
573
|
+
# Pick a flag to quiz on
|
|
574
|
+
available_flags = [f for f in parsed["flags"] if f in FLAG_DATABASE.get(base_cmd, {})]
|
|
575
|
+
if not available_flags:
|
|
576
|
+
return None
|
|
577
|
+
|
|
578
|
+
target_flag = random.choice(available_flags)
|
|
579
|
+
flag_desc = FLAG_DATABASE[base_cmd][target_flag]
|
|
580
|
+
|
|
581
|
+
# Generate distractor flags
|
|
582
|
+
distractor_flags = _generate_distractor_flags(base_cmd, target_flag, 3)
|
|
583
|
+
|
|
584
|
+
# Ensure we have exactly 3 distractors
|
|
585
|
+
while len(distractor_flags) < 3:
|
|
586
|
+
fake_flag = f"-{random.choice('abcdefghjkmnopqrstuwxyz')}"
|
|
587
|
+
if fake_flag not in distractor_flags and fake_flag != target_flag:
|
|
588
|
+
distractor_flags.append(fake_flag)
|
|
589
|
+
|
|
590
|
+
# Create options
|
|
591
|
+
all_flags = [target_flag] + distractor_flags[:3]
|
|
592
|
+
random.shuffle(all_flags)
|
|
593
|
+
|
|
594
|
+
options = []
|
|
595
|
+
correct_id = "a"
|
|
596
|
+
|
|
597
|
+
for i, flag in enumerate(all_flags):
|
|
598
|
+
opt_id = chr(ord('a') + i)
|
|
599
|
+
is_correct = (flag == target_flag)
|
|
600
|
+
if is_correct:
|
|
601
|
+
correct_id = opt_id
|
|
602
|
+
|
|
603
|
+
# Get description for option explanation
|
|
604
|
+
flag_explanation = FLAG_DATABASE.get(base_cmd, {}).get(flag, "Unknown flag")
|
|
605
|
+
|
|
606
|
+
options.append(QuizOption(
|
|
607
|
+
id=opt_id,
|
|
608
|
+
text=flag,
|
|
609
|
+
is_correct=is_correct,
|
|
610
|
+
explanation=f"{flag}: {flag_explanation}" if flag in FLAG_DATABASE.get(base_cmd, {}) else f"{flag}: Not a standard flag for {base_cmd}"
|
|
611
|
+
))
|
|
612
|
+
|
|
613
|
+
question_id = _generate_id(f"which_flag_{base_cmd}_{target_flag}")
|
|
614
|
+
|
|
615
|
+
return QuizQuestion(
|
|
616
|
+
id=question_id,
|
|
617
|
+
quiz_type=QuizType.WHICH_FLAG,
|
|
618
|
+
question_text=f"You want to **{flag_desc.lower()}** when using `{base_cmd}`. Which flag should you use?",
|
|
619
|
+
options=options,
|
|
620
|
+
correct_option_id=correct_id,
|
|
621
|
+
explanation=f"The `{target_flag}` flag in `{base_cmd}` is used to {flag_desc.lower()}",
|
|
622
|
+
difficulty=2,
|
|
623
|
+
command_context=base_cmd,
|
|
624
|
+
tags=[base_cmd, "which_flag"]
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def generate_build_command_quiz(
|
|
629
|
+
command: dict,
|
|
630
|
+
analyzed_data: Optional[dict] = None
|
|
631
|
+
) -> QuizQuestion:
|
|
632
|
+
"""
|
|
633
|
+
Generate a "Build the command" quiz question.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
command: Dictionary with command info including intent/description
|
|
637
|
+
analyzed_data: Optional additional analysis data
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
QuizQuestion instance
|
|
641
|
+
"""
|
|
642
|
+
cmd_string = command.get("command", "")
|
|
643
|
+
description = command.get("description", "")
|
|
644
|
+
intent = command.get("intent", description)
|
|
645
|
+
|
|
646
|
+
parsed = _parse_command(cmd_string)
|
|
647
|
+
base_cmd = parsed["base"]
|
|
648
|
+
|
|
649
|
+
# Create the correct command structure
|
|
650
|
+
correct_components = [base_cmd] + parsed["flags"] + parsed["args"]
|
|
651
|
+
correct_answer = " ".join(correct_components)
|
|
652
|
+
|
|
653
|
+
# Generate wrong arrangements
|
|
654
|
+
distractors = []
|
|
655
|
+
|
|
656
|
+
# Distractor 1: Wrong order
|
|
657
|
+
if len(correct_components) > 2:
|
|
658
|
+
wrong_order = correct_components.copy()
|
|
659
|
+
random.shuffle(wrong_order)
|
|
660
|
+
if wrong_order != correct_components:
|
|
661
|
+
distractors.append(" ".join(wrong_order))
|
|
662
|
+
|
|
663
|
+
# Distractor 2: Missing flag
|
|
664
|
+
if parsed["flags"]:
|
|
665
|
+
missing_flag = [base_cmd] + parsed["flags"][:-1] + parsed["args"]
|
|
666
|
+
distractors.append(" ".join(missing_flag))
|
|
667
|
+
|
|
668
|
+
# Distractor 3: Wrong flag
|
|
669
|
+
if parsed["flags"] and base_cmd in FLAG_DATABASE:
|
|
670
|
+
wrong_flags = _generate_distractor_flags(base_cmd, parsed["flags"][0], 1)
|
|
671
|
+
if wrong_flags:
|
|
672
|
+
wrong_flag_cmd = [base_cmd] + [wrong_flags[0]] + parsed["flags"][1:] + parsed["args"]
|
|
673
|
+
distractors.append(" ".join(wrong_flag_cmd))
|
|
674
|
+
|
|
675
|
+
# Distractor 4: Related but wrong command
|
|
676
|
+
related = _get_related_commands(base_cmd)
|
|
677
|
+
if related:
|
|
678
|
+
wrong_cmd = [related[0]] + parsed["flags"] + parsed["args"]
|
|
679
|
+
distractors.append(" ".join(wrong_cmd))
|
|
680
|
+
|
|
681
|
+
# Ensure we have exactly 3 distractors
|
|
682
|
+
while len(distractors) < 3:
|
|
683
|
+
# Add a clearly wrong option
|
|
684
|
+
distractors.append(f"{base_cmd} --invalid-option")
|
|
685
|
+
|
|
686
|
+
# Remove duplicates and correct answer from distractors
|
|
687
|
+
distractors = list(set(d for d in distractors if d != correct_answer))[:3]
|
|
688
|
+
while len(distractors) < 3:
|
|
689
|
+
distractors.append(f"{base_cmd} --wrong-flag")
|
|
690
|
+
|
|
691
|
+
# Create options
|
|
692
|
+
all_answers = [correct_answer] + distractors[:3]
|
|
693
|
+
random.shuffle(all_answers)
|
|
694
|
+
|
|
695
|
+
options = []
|
|
696
|
+
correct_id = "a"
|
|
697
|
+
|
|
698
|
+
for i, answer in enumerate(all_answers):
|
|
699
|
+
opt_id = chr(ord('a') + i)
|
|
700
|
+
is_correct = (answer == correct_answer)
|
|
701
|
+
if is_correct:
|
|
702
|
+
correct_id = opt_id
|
|
703
|
+
options.append(QuizOption(
|
|
704
|
+
id=opt_id,
|
|
705
|
+
text=f"`{answer}`",
|
|
706
|
+
is_correct=is_correct,
|
|
707
|
+
explanation="Correct command structure" if is_correct else "Incorrect command structure"
|
|
708
|
+
))
|
|
709
|
+
|
|
710
|
+
question_id = _generate_id(f"build_{cmd_string}")
|
|
711
|
+
|
|
712
|
+
task_description = intent if intent else f"perform the operation: {description}"
|
|
713
|
+
|
|
714
|
+
return QuizQuestion(
|
|
715
|
+
id=question_id,
|
|
716
|
+
quiz_type=QuizType.BUILD_COMMAND,
|
|
717
|
+
question_text=f"Build the correct command to **{task_description}**.\n\nWhich command is correct?",
|
|
718
|
+
options=options,
|
|
719
|
+
correct_option_id=correct_id,
|
|
720
|
+
explanation=f"The correct command is `{correct_answer}` - this properly accomplishes the task",
|
|
721
|
+
difficulty=3,
|
|
722
|
+
command_context=cmd_string,
|
|
723
|
+
tags=[base_cmd, "build_command"]
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def generate_spot_difference_quiz(
|
|
728
|
+
cmd1: dict,
|
|
729
|
+
cmd2: dict
|
|
730
|
+
) -> Optional[QuizQuestion]:
|
|
731
|
+
"""
|
|
732
|
+
Generate a "Spot the difference" quiz question.
|
|
733
|
+
|
|
734
|
+
Args:
|
|
735
|
+
cmd1: First command dictionary
|
|
736
|
+
cmd2: Second command dictionary (should be similar but different)
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
QuizQuestion instance or None if commands aren't suitable
|
|
740
|
+
"""
|
|
741
|
+
cmd1_string = cmd1.get("command", "")
|
|
742
|
+
cmd2_string = cmd2.get("command", "")
|
|
743
|
+
|
|
744
|
+
parsed1 = _parse_command(cmd1_string)
|
|
745
|
+
parsed2 = _parse_command(cmd2_string)
|
|
746
|
+
|
|
747
|
+
# Commands should have same base
|
|
748
|
+
if parsed1["base"] != parsed2["base"]:
|
|
749
|
+
return None
|
|
750
|
+
|
|
751
|
+
base_cmd = parsed1["base"]
|
|
752
|
+
|
|
753
|
+
# Find the differences
|
|
754
|
+
flags1 = set(parsed1["flags"])
|
|
755
|
+
flags2 = set(parsed2["flags"])
|
|
756
|
+
|
|
757
|
+
only_in_1 = flags1 - flags2
|
|
758
|
+
only_in_2 = flags2 - flags1
|
|
759
|
+
|
|
760
|
+
if not only_in_1 and not only_in_2:
|
|
761
|
+
# Check if args are different
|
|
762
|
+
if parsed1["args"] == parsed2["args"]:
|
|
763
|
+
return None
|
|
764
|
+
|
|
765
|
+
# Build the correct explanation of difference
|
|
766
|
+
differences = []
|
|
767
|
+
if only_in_1:
|
|
768
|
+
for flag in only_in_1:
|
|
769
|
+
desc = _get_flag_description(base_cmd, flag)
|
|
770
|
+
differences.append(f"Command 1 has `{flag}` ({desc or 'unknown'})")
|
|
771
|
+
if only_in_2:
|
|
772
|
+
for flag in only_in_2:
|
|
773
|
+
desc = _get_flag_description(base_cmd, flag)
|
|
774
|
+
differences.append(f"Command 2 has `{flag}` ({desc or 'unknown'})")
|
|
775
|
+
if parsed1["args"] != parsed2["args"]:
|
|
776
|
+
differences.append(f"Different arguments: '{' '.join(parsed1['args'])}' vs '{' '.join(parsed2['args'])}'")
|
|
777
|
+
|
|
778
|
+
correct_explanation = "; ".join(differences) if differences else "Different arguments or options"
|
|
779
|
+
|
|
780
|
+
# Generate distractor explanations
|
|
781
|
+
distractors = [
|
|
782
|
+
"Both commands do exactly the same thing",
|
|
783
|
+
f"Command 1 runs faster than Command 2",
|
|
784
|
+
f"Command 2 is deprecated, Command 1 is the modern version",
|
|
785
|
+
f"Command 1 modifies files, Command 2 only reads them",
|
|
786
|
+
f"Command 2 requires root permissions, Command 1 doesn't"
|
|
787
|
+
]
|
|
788
|
+
random.shuffle(distractors)
|
|
789
|
+
distractor_explanations = distractors[:3]
|
|
790
|
+
|
|
791
|
+
# Create options
|
|
792
|
+
all_answers = [correct_explanation] + distractor_explanations
|
|
793
|
+
random.shuffle(all_answers)
|
|
794
|
+
|
|
795
|
+
options = []
|
|
796
|
+
correct_id = "a"
|
|
797
|
+
|
|
798
|
+
for i, answer in enumerate(all_answers):
|
|
799
|
+
opt_id = chr(ord('a') + i)
|
|
800
|
+
is_correct = (answer == correct_explanation)
|
|
801
|
+
if is_correct:
|
|
802
|
+
correct_id = opt_id
|
|
803
|
+
options.append(QuizOption(
|
|
804
|
+
id=opt_id,
|
|
805
|
+
text=answer,
|
|
806
|
+
is_correct=is_correct,
|
|
807
|
+
explanation="Correct analysis" if is_correct else "Incorrect analysis"
|
|
808
|
+
))
|
|
809
|
+
|
|
810
|
+
question_id = _generate_id(f"spot_diff_{cmd1_string}_{cmd2_string}")
|
|
811
|
+
|
|
812
|
+
return QuizQuestion(
|
|
813
|
+
id=question_id,
|
|
814
|
+
quiz_type=QuizType.SPOT_DIFFERENCE,
|
|
815
|
+
question_text=f"What is the key difference between these two commands?\n\n**Command 1:**\n```bash\n{cmd1_string}\n```\n\n**Command 2:**\n```bash\n{cmd2_string}\n```",
|
|
816
|
+
options=options,
|
|
817
|
+
correct_option_id=correct_id,
|
|
818
|
+
explanation=f"The key difference is: {correct_explanation}",
|
|
819
|
+
difficulty=4,
|
|
820
|
+
command_context=f"{cmd1_string} vs {cmd2_string}",
|
|
821
|
+
tags=[base_cmd, "spot_difference"]
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _create_similar_command_variant(command: dict) -> Optional[dict]:
|
|
826
|
+
"""Create a similar but different command for spot-the-difference."""
|
|
827
|
+
cmd_string = command.get("command", "")
|
|
828
|
+
parsed = _parse_command(cmd_string)
|
|
829
|
+
base_cmd = parsed["base"]
|
|
830
|
+
|
|
831
|
+
if base_cmd not in FLAG_DATABASE:
|
|
832
|
+
return None
|
|
833
|
+
|
|
834
|
+
# Strategy: add, remove, or change a flag
|
|
835
|
+
strategies = []
|
|
836
|
+
|
|
837
|
+
# Can add a flag
|
|
838
|
+
available_flags = [f for f in FLAG_DATABASE[base_cmd].keys() if f not in parsed["flags"]]
|
|
839
|
+
if available_flags:
|
|
840
|
+
strategies.append("add")
|
|
841
|
+
|
|
842
|
+
# Can remove a flag
|
|
843
|
+
if parsed["flags"]:
|
|
844
|
+
strategies.append("remove")
|
|
845
|
+
|
|
846
|
+
# Can change a flag
|
|
847
|
+
if parsed["flags"] and available_flags:
|
|
848
|
+
strategies.append("change")
|
|
849
|
+
|
|
850
|
+
if not strategies:
|
|
851
|
+
return None
|
|
852
|
+
|
|
853
|
+
strategy = random.choice(strategies)
|
|
854
|
+
new_flags = parsed["flags"].copy()
|
|
855
|
+
|
|
856
|
+
if strategy == "add" and available_flags:
|
|
857
|
+
new_flags.append(random.choice(available_flags))
|
|
858
|
+
elif strategy == "remove" and new_flags:
|
|
859
|
+
new_flags.pop(random.randint(0, len(new_flags) - 1))
|
|
860
|
+
elif strategy == "change" and new_flags and available_flags:
|
|
861
|
+
idx = random.randint(0, len(new_flags) - 1)
|
|
862
|
+
new_flags[idx] = random.choice(available_flags)
|
|
863
|
+
|
|
864
|
+
new_cmd = " ".join([base_cmd] + new_flags + parsed["args"])
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
"command": new_cmd,
|
|
868
|
+
"description": f"Variant of {cmd_string}",
|
|
869
|
+
"complexity": command.get("complexity", 2)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def generate_quiz_set(
|
|
874
|
+
analyzed_commands: list[dict],
|
|
875
|
+
count: int = 20
|
|
876
|
+
) -> list[QuizQuestion]:
|
|
877
|
+
"""
|
|
878
|
+
Generate a set of quiz questions from analyzed commands.
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
analyzed_commands: List of analyzed command dictionaries
|
|
882
|
+
count: Target number of questions (default 20)
|
|
883
|
+
|
|
884
|
+
Returns:
|
|
885
|
+
List of QuizQuestion instances
|
|
886
|
+
"""
|
|
887
|
+
questions: list[QuizQuestion] = []
|
|
888
|
+
|
|
889
|
+
# Filter commands by complexity >= 2
|
|
890
|
+
eligible_commands = [
|
|
891
|
+
cmd for cmd in analyzed_commands
|
|
892
|
+
if cmd.get("complexity", 0) >= 2
|
|
893
|
+
]
|
|
894
|
+
|
|
895
|
+
if not eligible_commands:
|
|
896
|
+
eligible_commands = analyzed_commands
|
|
897
|
+
|
|
898
|
+
# Weight toward high-frequency commands
|
|
899
|
+
weighted_commands = []
|
|
900
|
+
for cmd in eligible_commands:
|
|
901
|
+
frequency = cmd.get("frequency", 1)
|
|
902
|
+
weight = min(frequency, 5) # Cap weight at 5
|
|
903
|
+
weighted_commands.extend([cmd] * weight)
|
|
904
|
+
|
|
905
|
+
if not weighted_commands:
|
|
906
|
+
weighted_commands = eligible_commands
|
|
907
|
+
|
|
908
|
+
# Calculate target counts for each quiz type
|
|
909
|
+
# Distribution: 40% what_does, 25% which_flag, 20% build, 15% spot_diff
|
|
910
|
+
target_what_does = max(1, int(count * 0.4))
|
|
911
|
+
target_which_flag = max(1, int(count * 0.25))
|
|
912
|
+
target_build = max(1, int(count * 0.2))
|
|
913
|
+
target_spot_diff = max(1, int(count * 0.15))
|
|
914
|
+
|
|
915
|
+
used_commands = set()
|
|
916
|
+
|
|
917
|
+
# Generate "What does this do?" questions
|
|
918
|
+
random.shuffle(weighted_commands)
|
|
919
|
+
for cmd in weighted_commands:
|
|
920
|
+
if len([q for q in questions if q.quiz_type == QuizType.WHAT_DOES]) >= target_what_does:
|
|
921
|
+
break
|
|
922
|
+
cmd_id = cmd.get("command", "")
|
|
923
|
+
if cmd_id not in used_commands:
|
|
924
|
+
q = generate_what_does_quiz(cmd)
|
|
925
|
+
questions.append(q)
|
|
926
|
+
used_commands.add(cmd_id)
|
|
927
|
+
|
|
928
|
+
# Generate "Which flag?" questions
|
|
929
|
+
random.shuffle(weighted_commands)
|
|
930
|
+
for cmd in weighted_commands:
|
|
931
|
+
if len([q for q in questions if q.quiz_type == QuizType.WHICH_FLAG]) >= target_which_flag:
|
|
932
|
+
break
|
|
933
|
+
q = generate_which_flag_quiz(cmd)
|
|
934
|
+
if q:
|
|
935
|
+
questions.append(q)
|
|
936
|
+
|
|
937
|
+
# Generate "Build the command" questions
|
|
938
|
+
random.shuffle(weighted_commands)
|
|
939
|
+
for cmd in weighted_commands:
|
|
940
|
+
if len([q for q in questions if q.quiz_type == QuizType.BUILD_COMMAND]) >= target_build:
|
|
941
|
+
break
|
|
942
|
+
q = generate_build_command_quiz(cmd)
|
|
943
|
+
questions.append(q)
|
|
944
|
+
|
|
945
|
+
# Generate "Spot the difference" questions
|
|
946
|
+
random.shuffle(weighted_commands)
|
|
947
|
+
for cmd in weighted_commands:
|
|
948
|
+
if len([q for q in questions if q.quiz_type == QuizType.SPOT_DIFFERENCE]) >= target_spot_diff:
|
|
949
|
+
break
|
|
950
|
+
variant = _create_similar_command_variant(cmd)
|
|
951
|
+
if variant:
|
|
952
|
+
q = generate_spot_difference_quiz(cmd, variant)
|
|
953
|
+
if q:
|
|
954
|
+
questions.append(q)
|
|
955
|
+
|
|
956
|
+
# Shuffle final questions
|
|
957
|
+
random.shuffle(questions)
|
|
958
|
+
|
|
959
|
+
# Trim to target count
|
|
960
|
+
return questions[:count]
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def create_quiz(
|
|
964
|
+
analyzed_commands: list[dict],
|
|
965
|
+
title: str = "Bash Command Quiz",
|
|
966
|
+
description: str = "Test your bash command knowledge",
|
|
967
|
+
question_count: int = 20,
|
|
968
|
+
time_limit: Optional[int] = None
|
|
969
|
+
) -> Quiz:
|
|
970
|
+
"""
|
|
971
|
+
Create a complete quiz from analyzed commands.
|
|
972
|
+
|
|
973
|
+
Args:
|
|
974
|
+
analyzed_commands: List of analyzed command dictionaries
|
|
975
|
+
title: Quiz title
|
|
976
|
+
description: Quiz description
|
|
977
|
+
question_count: Target number of questions
|
|
978
|
+
time_limit: Optional time limit in seconds
|
|
979
|
+
|
|
980
|
+
Returns:
|
|
981
|
+
Quiz instance
|
|
982
|
+
"""
|
|
983
|
+
questions = generate_quiz_set(analyzed_commands, question_count)
|
|
984
|
+
|
|
985
|
+
quiz_id = _generate_id(f"{title}_{len(questions)}")
|
|
986
|
+
|
|
987
|
+
return Quiz(
|
|
988
|
+
id=quiz_id,
|
|
989
|
+
title=title,
|
|
990
|
+
description=description,
|
|
991
|
+
questions=questions,
|
|
992
|
+
time_limit_seconds=time_limit
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
# =============================================================================
|
|
997
|
+
# Main entry point for testing
|
|
998
|
+
# =============================================================================
|
|
999
|
+
|
|
1000
|
+
if __name__ == "__main__":
|
|
1001
|
+
import json
|
|
1002
|
+
|
|
1003
|
+
# Example analyzed commands for testing
|
|
1004
|
+
sample_commands = [
|
|
1005
|
+
{
|
|
1006
|
+
"command": "ls -la /var/log",
|
|
1007
|
+
"description": "List all files including hidden ones in /var/log with detailed info",
|
|
1008
|
+
"complexity": 3,
|
|
1009
|
+
"frequency": 5
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
"command": "grep -rn 'error' /var/log",
|
|
1013
|
+
"description": "Search recursively for 'error' with line numbers in /var/log",
|
|
1014
|
+
"complexity": 3,
|
|
1015
|
+
"frequency": 4
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
"command": "find . -name '*.py' -type f",
|
|
1019
|
+
"description": "Find all Python files in current directory",
|
|
1020
|
+
"complexity": 4,
|
|
1021
|
+
"frequency": 3
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
"command": "tar -czvf backup.tar.gz /home/user",
|
|
1025
|
+
"description": "Create a gzipped tar archive of /home/user",
|
|
1026
|
+
"complexity": 4,
|
|
1027
|
+
"frequency": 2
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
"command": "chmod -R 755 /var/www",
|
|
1031
|
+
"description": "Recursively set permissions to 755 on /var/www",
|
|
1032
|
+
"complexity": 3,
|
|
1033
|
+
"frequency": 2
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
"command": "curl -sL https://example.com/api",
|
|
1037
|
+
"description": "Silently fetch URL following redirects",
|
|
1038
|
+
"complexity": 3,
|
|
1039
|
+
"frequency": 4
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
"command": "ps aux | grep python",
|
|
1043
|
+
"description": "List all processes and filter for python",
|
|
1044
|
+
"complexity": 3,
|
|
1045
|
+
"frequency": 5
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
"command": "sed -i 's/old/new/g' file.txt",
|
|
1049
|
+
"description": "Replace all occurrences of 'old' with 'new' in-place",
|
|
1050
|
+
"complexity": 4,
|
|
1051
|
+
"frequency": 2
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
"command": "sort -u names.txt",
|
|
1055
|
+
"description": "Sort file and remove duplicate lines",
|
|
1056
|
+
"complexity": 2,
|
|
1057
|
+
"frequency": 3
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
"command": "du -sh /home/*",
|
|
1061
|
+
"description": "Show disk usage summary for each home directory",
|
|
1062
|
+
"complexity": 2,
|
|
1063
|
+
"frequency": 4
|
|
1064
|
+
}
|
|
1065
|
+
]
|
|
1066
|
+
|
|
1067
|
+
# Generate quiz
|
|
1068
|
+
quiz = create_quiz(
|
|
1069
|
+
sample_commands,
|
|
1070
|
+
title="Bash Fundamentals Quiz",
|
|
1071
|
+
description="Test your knowledge of common bash commands",
|
|
1072
|
+
question_count=15
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
# Print quiz as JSON
|
|
1076
|
+
print(json.dumps(quiz.to_dict(), indent=2))
|
|
1077
|
+
|
|
1078
|
+
print(f"\n=== Generated {len(quiz.questions)} questions ===")
|
|
1079
|
+
for i, q in enumerate(quiz.questions, 1):
|
|
1080
|
+
print(f"\n{i}. [{q.quiz_type.value}] {q.question_text[:80]}...")
|