diff --git a/06-linuxplus-quiz-mode.el b/06-linuxplus-quiz-mode.el index 8c6393b..39ba1e6 100644 --- a/06-linuxplus-quiz-mode.el +++ b/06-linuxplus-quiz-mode.el @@ -24,18 +24,14 @@ (plist-get question :choices)) (defun linuxplus-quiz--current-question () - (nth linuxplus-quiz-current-question - linuxplus-quiz-questions)) + (nth linuxplus-quiz-current-question linuxplus-quiz-questions)) (defun linuxplus-quiz--current-user-answer () - (nth linuxplus-quiz-current-question - linuxplus-quiz-user-answers)) + (nth linuxplus-quiz-current-question linuxplus-quiz-user-answers)) (defun linuxplus-quiz--set-current-user-answer (answer) - (setf - (nth linuxplus-quiz-current-question - linuxplus-quiz-user-answers) - answer)) + (setf (nth linuxplus-quiz-current-question linuxplus-quiz-user-answers) + answer)) (defun linuxplus-quiz--answer-marker (answer) (cond @@ -56,18 +52,14 @@ ((null answer) "unanswered") (t (format "%S" answer)))) -(defun linuxplus-quiz--correct-answer-for-display - (question) - (let ((type - (linuxplus-quiz--question-type question))) +(defun linuxplus-quiz--correct-answer-for-display (question) + (let ((type (linuxplus-quiz--question-type question))) (cond ((memq type '(true-false yes-no)) (linuxplus-quiz--answer-string (linuxplus-quiz--question-answer question))) ((eq type 'multiple) - (format "%S" - (linuxplus-quiz--question-answers - question))) + (format "%S" (linuxplus-quiz--question-answers question))) (t "unknown")))) (defun linuxplus-quiz--advance-question () @@ -83,544 +75,235 @@ (1- linuxplus-quiz-current-question)) (setq linuxplus-quiz-current-choice 0))) -(defun linuxplus-quiz--multiple-selected-p - (answer choice-index) +(defun linuxplus-quiz--multiple-selected-p (answer choice-index) (and (listp answer) (memq choice-index answer))) +(defun linuxplus-quiz--toggle-current-choice () + (let* ((answer (copy-sequence + (or (linuxplus-quiz--current-user-answer) nil))) + (choice linuxplus-quiz-current-choice)) + (if (memq choice answer) + (setq answer (delq choice answer)) + (push choice answer)) + (linuxplus-quiz--set-current-user-answer + (sort answer #'<)))) + (defun linuxplus-quiz--advance-choice () - "Advance to next multiple-choice bracket." - (let* ((question - (linuxplus-quiz--current-question)) - (choices - (linuxplus-quiz--question-choices - question))) + "Advance to the next multiple-choice bracket if one exists." + (let* ((question (linuxplus-quiz--current-question)) + (choices (linuxplus-quiz--question-choices question))) (when (and choices (< linuxplus-quiz-current-choice (1- (length choices)))) (setq linuxplus-quiz-current-choice (1+ linuxplus-quiz-current-choice))))) -(defun linuxplus-quiz--toggle-current-choice () - "Toggle current multiple-choice selection." - (let* ((answer - (copy-sequence - (or - (linuxplus-quiz--current-user-answer) - nil))) - (choice linuxplus-quiz-current-choice) - (was-selected - (memq choice answer))) - - (if was-selected - - ;; Remove selection and stay. - (setq answer - (delq choice answer)) - - ;; Add selection and advance. - (push choice answer) - - (setq answer - (sort answer #'<)) - - (linuxplus-quiz--set-current-user-answer - answer) - - (linuxplus-quiz--advance-choice) - - ;; Stop removal logic. - (setq answer nil)) - - ;; Removal path only. - (when answer - (setq answer - (sort answer #'<)) - - (linuxplus-quiz--set-current-user-answer - answer)))) - -(defun linuxplus-quiz--answer-correct-p - (question user-answer) - - (let ((type - (linuxplus-quiz--question-type question))) - +(defun linuxplus-quiz--answer-correct-p (question user-answer) + (let ((type (linuxplus-quiz--question-type question))) (cond - ((memq type '(true-false yes-no)) - (eq user-answer - (linuxplus-quiz--question-answer - question))) - + (eq user-answer (linuxplus-quiz--question-answer question))) ((eq type 'multiple) (equal - (sort - (copy-sequence - (or user-answer nil)) - #'<) - - (sort - (copy-sequence - (linuxplus-quiz--question-answers - question)) - #'<))) - + (sort (copy-sequence (or user-answer nil)) #'<) + (sort (copy-sequence + (linuxplus-quiz--question-answers question)) + #'<))) (t nil)))) (defun linuxplus-quiz--question-index-at-point () "Return zero-based question index at point." (save-excursion (let ((found nil)) - (when - (re-search-backward - "^\\([0-9]+\\)\\.$" - nil t) - - (setq found - (1- - (string-to-number - (match-string 1))))) - - (when - (and found - (>= found 0) - (< found - (length - linuxplus-quiz-questions))) + (when (re-search-backward "^\\([0-9]+\\)\\.$" nil t) + (setq found (1- (string-to-number (match-string 1))))) + (when (and found + (>= found 0) + (< found (length linuxplus-quiz-questions))) found)))) -(defun linuxplus-quiz--choice-index-at-point () - "Return zero-based multiple-choice bracket index at point." - - (save-excursion - - (let ((current-line - (line-beginning-position)) - - (question-start nil) - - (choice-index 0) - - result) - - ;; Find question start. - (when - (re-search-backward - "^\\([0-9]+\\)\\.$" - nil t) - - (setq question-start (point))) - - (when question-start - - ;; Start searching after question header. - (forward-line 1) - - ;; Walk brackets. - (while - (and - (null result) - (re-search-forward - "\\[[^]]*\\]" - nil t)) - - ;; Did we hit current line? - (when - (= (line-beginning-position) - current-line) - - (setq result choice-index)) - - ;; Next bracket index. - (setq choice-index - (1+ choice-index)))) - - result))) - (defun linuxplus-quiz--sync-current-position-from-point () - "Update current question and multiple-choice bracket from cursor position." - - (let ((question-index - (linuxplus-quiz--question-index-at-point))) - + "Update current question from cursor position." + (let ((question-index (linuxplus-quiz--question-index-at-point))) (when question-index - - (setq linuxplus-quiz-current-question - question-index) - - (if - (eq - (linuxplus-quiz--question-type - (linuxplus-quiz--current-question)) - 'multiple) - - (setq linuxplus-quiz-current-choice - (or - (linuxplus-quiz--choice-index-at-point) - 0)) - + (setq linuxplus-quiz-current-question question-index) + (unless (eq (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'multiple) (setq linuxplus-quiz-current-choice 0))))) (defun linuxplus-quiz--invalid-input-message () - (linuxplus-quiz--sync-current-position-from-point) - - (let ((type - (linuxplus-quiz--question-type - (linuxplus-quiz--current-question)))) - + (let ((type (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)))) (cond - ((eq type 'true-false) - (message - "Valid input: t for true, f for false.")) - + (message "Valid input: t for true, f for false.")) ((eq type 'yes-no) - (message - "Valid input: y for yes, n for no.")) - + (message "Valid input: y for yes, n for no.")) ((eq type 'multiple) - (message - "Use the space bar to add/remove the * character.")) - + (message "Use the space bar to add/remove the * character.")) (t (message "Invalid input."))))) -(defun linuxplus-quiz--set-answer-and-maybe-advance - (answer) - - "Set ANSWER. -If unanswered, advance. -If replacing, stay." - +(defun linuxplus-quiz--set-answer-and-maybe-advance (answer) + "Set ANSWER. If unanswered, advance; if replacing, stay." (linuxplus-quiz--sync-current-position-from-point) - - (let ((old-answer - (linuxplus-quiz--current-user-answer))) - - (linuxplus-quiz--set-current-user-answer - answer) - + (let ((old-answer (linuxplus-quiz--current-user-answer))) + (linuxplus-quiz--set-current-user-answer answer) (unless old-answer (linuxplus-quiz--advance-question)) - (linuxplus-quiz-render))) (defun linuxplus-quiz-render () - (let ((inhibit-read-only t)) - (erase-buffer) - (insert "Linux+ Quiz\n") (insert "===========\n\n") - (let ((current-type (linuxplus-quiz--question-type (linuxplus-quiz--current-question)))) - (cond - ((eq current-type 'true-false) - (insert - "Type 't' for true or 'f' for false inside the brackets.\n")) - + (insert "Type 't' for true or 'f' for false inside the brackets.\n")) ((eq current-type 'yes-no) - (insert - "Type 'y' for yes or 'n' for no inside the brackets.\n")) - + (insert "Type 'y' for yes or 'n' for no inside the brackets.\n")) ((eq current-type 'multiple) - (insert - "Use the space bar to add/remove the * character for multiple choice questions.\n")) - + (insert "Use the space bar to add/remove the * character for multiple choice questions.\n")) (t (insert "Answer the question.\n")))) + (insert "b = back, TAB = forward, x = finish, q = quit\n\n") - (insert - "b = back, TAB = forward, x = finish, q = quit\n\n") - - (dotimes - (i (length linuxplus-quiz-questions)) - - (let* ((question - (nth i linuxplus-quiz-questions)) - - (type - (linuxplus-quiz--question-type - question)) - - (prompt - (linuxplus-quiz--question-prompt - question)) - - (user-answer - (nth i linuxplus-quiz-user-answers)) - - (current-question-p - (= i - linuxplus-quiz-current-question))) - - (insert - (format "%d.\n" (1+ i))) - - (insert - (format " %s\n" prompt)) - + (dotimes (i (length linuxplus-quiz-questions)) + (let* ((question (nth i linuxplus-quiz-questions)) + (type (linuxplus-quiz--question-type question)) + (prompt (linuxplus-quiz--question-prompt question)) + (user-answer (nth i linuxplus-quiz-user-answers)) + (current-question-p (= i linuxplus-quiz-current-question))) + (insert (format "%d.\n" (1+ i))) + (insert (format " %s\n" prompt)) (cond - ((eq type 'true-false) - (insert - (format - "%s %s True or False\n\n" - - (if current-question-p - ">" - " ") - - (linuxplus-quiz--answer-marker - user-answer)))) - + (format "%s %s True or False\n\n" + (if current-question-p ">" " ") + (linuxplus-quiz--answer-marker user-answer)))) ((eq type 'yes-no) - (insert - (format - "%s %s Yes or No\n\n" - - (if current-question-p - ">" - " ") - - (linuxplus-quiz--answer-marker - user-answer)))) - + (format "%s %s Yes or No\n\n" + (if current-question-p ">" " ") + (linuxplus-quiz--answer-marker user-answer)))) ((eq type 'multiple) - - (let ((choices - (linuxplus-quiz--question-choices - question))) - - (dotimes - (choice-index - (length choices)) - - (let* - ((selected - (if - (linuxplus-quiz--multiple-selected-p - user-answer - choice-index) - 'selected - nil)) - - (choice-current-p - (and - current-question-p - (= choice-index - linuxplus-quiz-current-choice)))) - + (let ((choices (linuxplus-quiz--question-choices question))) + (dotimes (choice-index (length choices)) + (let* ((selected + (if (linuxplus-quiz--multiple-selected-p + user-answer choice-index) + 'selected + nil)) + (choice-current-p + (and current-question-p + (= choice-index linuxplus-quiz-current-choice)))) (insert - (format - "%s %s %s\n" - - (if choice-current-p - ">" - " ") - - (linuxplus-quiz--answer-marker - selected) - - (nth choice-index - choices)))))) - - (insert "\n"))))) - - (insert - "\nReminder: x = finish quiz, q = quit/return to lesson.\n")) - + (format "%s %s %s\n" + (if choice-current-p ">" " ") + (linuxplus-quiz--answer-marker selected) + (nth choice-index choices)))))) + (insert "\n")))))) (linuxplus-quiz-goto-current-answer)) (defun linuxplus-quiz-goto-current-answer () - (goto-char (point-min)) - (let ((target-question (format "^%d\\.$" - (1+ - linuxplus-quiz-current-question)))) - - (when - (re-search-forward - target-question - nil t) - - (let ((type - (linuxplus-quiz--question-type - (linuxplus-quiz--current-question)))) - + (1+ linuxplus-quiz-current-question)))) + (when (re-search-forward target-question nil t) + (let ((type (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)))) (cond - ((memq type '(true-false yes-no)) - - (when - (re-search-forward - "\\[[^]]*\\]" - nil t) - + (when (re-search-forward "\\[[^]]*\\]" nil t) (backward-char 1))) - ((eq type 'multiple) - - (dotimes - (_ linuxplus-quiz-current-choice) - - (re-search-forward - "\\[[^]]*\\]" - nil t)) - - (when - (re-search-forward - "\\[[^]]*\\]" - nil t) - + (dotimes (_ linuxplus-quiz-current-choice) + (re-search-forward "\\[[^]]*\\]" nil t)) + (when (re-search-forward "\\[[^]]*\\]" nil t) (backward-char 1)))))))) (defun linuxplus-quiz-answer-true () - (interactive) - (linuxplus-quiz--sync-current-position-from-point) - - (if - (eq - (linuxplus-quiz--question-type - (linuxplus-quiz--current-question)) - 'true-false) - - (linuxplus-quiz--set-answer-and-maybe-advance - 'true) - + (if (eq (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'true-false) + (linuxplus-quiz--set-answer-and-maybe-advance 'true) (linuxplus-quiz--invalid-input-message))) (defun linuxplus-quiz-answer-false () - (interactive) - (linuxplus-quiz--sync-current-position-from-point) - - (if - (eq - (linuxplus-quiz--question-type - (linuxplus-quiz--current-question)) - 'true-false) - - (linuxplus-quiz--set-answer-and-maybe-advance - 'false) - + (if (eq (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'true-false) + (linuxplus-quiz--set-answer-and-maybe-advance 'false) (linuxplus-quiz--invalid-input-message))) (defun linuxplus-quiz-answer-yes () - (interactive) - (linuxplus-quiz--sync-current-position-from-point) - - (if - (eq - (linuxplus-quiz--question-type - (linuxplus-quiz--current-question)) - 'yes-no) - - (linuxplus-quiz--set-answer-and-maybe-advance - 'yes) - + (if (eq (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'yes-no) + (linuxplus-quiz--set-answer-and-maybe-advance 'yes) (linuxplus-quiz--invalid-input-message))) (defun linuxplus-quiz-answer-no () - (interactive) - (linuxplus-quiz--sync-current-position-from-point) - - (if - (eq - (linuxplus-quiz--question-type - (linuxplus-quiz--current-question)) - 'yes-no) - - (linuxplus-quiz--set-answer-and-maybe-advance - 'no) - + (if (eq (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'yes-no) + (linuxplus-quiz--set-answer-and-maybe-advance 'no) (linuxplus-quiz--invalid-input-message))) (defun linuxplus-quiz-toggle-multiple () - (interactive) - (linuxplus-quiz--sync-current-position-from-point) - - (if - (eq - (linuxplus-quiz--question-type - (linuxplus-quiz--current-question)) - 'multiple) - + (if (eq (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'multiple) (progn (linuxplus-quiz--toggle-current-choice) + (linuxplus-quiz--advance-choice) (linuxplus-quiz-render)) - (linuxplus-quiz--invalid-input-message))) (defun linuxplus-quiz-next () - (interactive) - (linuxplus-quiz--sync-current-position-from-point) - - (let ((type - (linuxplus-quiz--question-type - (linuxplus-quiz--current-question)))) - - (if - (and - (eq type 'multiple) - - (< linuxplus-quiz-current-choice - (1- - (length - (linuxplus-quiz--question-choices - (linuxplus-quiz--current-question)))))) - + (let ((type (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)))) + (if (and (eq type 'multiple) + (< linuxplus-quiz-current-choice + (1- (length + (linuxplus-quiz--question-choices + (linuxplus-quiz--current-question)))))) (setq linuxplus-quiz-current-choice - (1+ - linuxplus-quiz-current-choice)) - + (1+ linuxplus-quiz-current-choice)) (linuxplus-quiz--advance-question))) - (linuxplus-quiz-render)) (defun linuxplus-quiz-prev () - (interactive) - (linuxplus-quiz--sync-current-position-from-point) - - (let ((type - (linuxplus-quiz--question-type - (linuxplus-quiz--current-question)))) - - (if - (and - (eq type 'multiple) - (> linuxplus-quiz-current-choice 0)) - + (let ((type (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)))) + (if (and (eq type 'multiple) + (> linuxplus-quiz-current-choice 0)) (setq linuxplus-quiz-current-choice - (1- - linuxplus-quiz-current-choice)) - + (1- linuxplus-quiz-current-choice)) (linuxplus-quiz--previous-question))) - (linuxplus-quiz-render)) (defun linuxplus-quiz-invalid-key () @@ -628,185 +311,71 @@ If replacing, stay." (linuxplus-quiz--invalid-input-message)) (defun linuxplus-quiz-return-to-lesson () - - "Return to lesson/window layout." - + "Return to the lesson/window layout from before the quiz." (interactive) - - (let ((config - linuxplus-quiz-return-window-configuration) - - (buffer - linuxplus-quiz-return-buffer)) - - (when - (window-configuration-p config) - + (let ((config linuxplus-quiz-return-window-configuration) + (buffer linuxplus-quiz-return-buffer)) + (when (window-configuration-p config) (set-window-configuration config)) - - (when - (buffer-live-p buffer) - + (when (buffer-live-p buffer) (switch-to-buffer buffer)))) (defun linuxplus-quiz-finish () - (interactive) - (let ((score 0) - - (total - (length linuxplus-quiz-questions))) - + (total (length linuxplus-quiz-questions))) (dotimes (i total) - - (when - (linuxplus-quiz--answer-correct-p - (nth i linuxplus-quiz-questions) - (nth i linuxplus-quiz-user-answers)) - + (when (linuxplus-quiz--answer-correct-p + (nth i linuxplus-quiz-questions) + (nth i linuxplus-quiz-user-answers)) (setq score (1+ score)))) - - (linuxplus-record-score - linuxplus-quiz-lesson-name - score) - + (linuxplus-record-score linuxplus-quiz-lesson-name score) (let ((inhibit-read-only t)) - (erase-buffer) - (insert "Linux+ Quiz Results\n") (insert "===================\n\n") - - (insert - (format - "Lesson: %s\n" - linuxplus-quiz-lesson-name)) - - (insert - (format - "Score: %d/%d\n\n" - score total)) - + (insert (format "Lesson: %s\n" linuxplus-quiz-lesson-name)) + (insert (format "Score: %d/%d\n\n" score total)) (insert "Detailed review:\n\n") - (dotimes (i total) - - (let* - ((question - (nth i linuxplus-quiz-questions)) - - (prompt - (linuxplus-quiz--question-prompt - question)) - - (user-answer - (nth i linuxplus-quiz-user-answers))) - - (insert - (format - "%d. %s\n" - (1+ i) - prompt)) - - (insert - (format - " Your answer: %s\n" - (linuxplus-quiz--answer-string - user-answer))) - - (insert - (format - " Correct answer: %s\n" - (linuxplus-quiz--correct-answer-for-display - question))) - - (insert - (format - " Result: %s\n\n" - - (if - (linuxplus-quiz--answer-correct-p - question - user-answer) - - "correct" - - "incorrect"))))) - - (insert - "Press q to return to the lesson.\n")) - + (let* ((question (nth i linuxplus-quiz-questions)) + (prompt (linuxplus-quiz--question-prompt question)) + (user-answer (nth i linuxplus-quiz-user-answers))) + (insert (format "%d. %s\n" (1+ i) prompt)) + (insert (format " Your answer: %s\n" + (linuxplus-quiz--answer-string user-answer))) + (insert (format " Correct answer: %s\n" + (linuxplus-quiz--correct-answer-for-display question))) + (insert (format " Result: %s\n\n" + (if (linuxplus-quiz--answer-correct-p + question user-answer) + "correct" + "incorrect"))))) + (insert "Press q to return to the lesson.\n")) (goto-char (point-min)) - (linuxplus-quiz-mode) - - (message - "Quiz complete. Score: %d/%d" - score total))) + (message "Quiz complete. Score: %d/%d" score total))) (setq linuxplus-quiz-mode-map - - (let ((map - (make-sparse-keymap))) - - ;; invalid printable input - + (let ((map (make-sparse-keymap))) + ;; Invalid printable input by default. (dotimes (i 95) + (define-key map + (char-to-string (+ i 32)) + #'linuxplus-quiz-invalid-key)) - (define-key - map - (char-to-string (+ i 32)) - #'linuxplus-quiz-invalid-key)) + ;; Valid answers. + (define-key map (kbd "t") #'linuxplus-quiz-answer-true) + (define-key map (kbd "f") #'linuxplus-quiz-answer-false) + (define-key map (kbd "y") #'linuxplus-quiz-answer-yes) + (define-key map (kbd "n") #'linuxplus-quiz-answer-no) + (define-key map (kbd "SPC") #'linuxplus-quiz-toggle-multiple) - ;; valid answers - - (define-key - map - (kbd "t") - #'linuxplus-quiz-answer-true) - - (define-key - map - (kbd "f") - #'linuxplus-quiz-answer-false) - - (define-key - map - (kbd "y") - #'linuxplus-quiz-answer-yes) - - (define-key - map - (kbd "n") - #'linuxplus-quiz-answer-no) - - (define-key - map - (kbd "SPC") - #'linuxplus-quiz-toggle-multiple) - - ;; navigation - - (define-key - map - (kbd "b") - #'linuxplus-quiz-prev) - - (define-key - map - (kbd "TAB") - #'linuxplus-quiz-next) - - (define-key - map - (kbd "x") - #'linuxplus-quiz-finish) - - (define-key - map - (kbd "q") - #'linuxplus-quiz-return-to-lesson) + ;; Navigation / control. + (define-key map (kbd "b") #'linuxplus-quiz-prev) + (define-key map (kbd "TAB") #'linuxplus-quiz-next) + (define-key map (kbd "x") #'linuxplus-quiz-finish) + (define-key map (kbd "q") #'linuxplus-quiz-return-to-lesson) map)) @@ -815,50 +384,24 @@ If replacing, stay." special-mode "LinuxPlus-Quiz" "Major mode for Linux+ quizzes." - (setq buffer-read-only t)) -(defun linuxplus-start-quiz - (lesson questions buffer) - - "Start Linux+ quiz in full window." - - (let ((return-config - (current-window-configuration)) - - (return-buffer - (current-buffer)) - - (buf - (get-buffer-create buffer))) - +(defun linuxplus-start-quiz (lesson questions buffer) + "Start a Linux+ quiz in a full window." + (let ((return-config (current-window-configuration)) + (return-buffer (current-buffer)) + (buf (get-buffer-create buffer))) (delete-other-windows) - (switch-to-buffer buf) - (linuxplus-quiz-mode) - - (setq linuxplus-quiz-return-window-configuration - return-config) - - (setq linuxplus-quiz-return-buffer - return-buffer) - - (setq linuxplus-quiz-lesson-name - lesson) - - (setq linuxplus-quiz-questions - questions) - + (setq linuxplus-quiz-return-window-configuration return-config) + (setq linuxplus-quiz-return-buffer return-buffer) + (setq linuxplus-quiz-lesson-name lesson) + (setq linuxplus-quiz-questions questions) (setq linuxplus-quiz-user-answers - (make-list - (length questions) - nil)) - + (make-list (length questions) nil)) (setq linuxplus-quiz-current-question 0) - (setq linuxplus-quiz-current-choice 0) - (linuxplus-quiz-render))) ;;; 06-linuxplus-quiz-mode.el ends here diff --git a/100-systemd-service-lesson.el b/100-systemd-service-lesson.el new file mode 100644 index 0000000..165f3a0 --- /dev/null +++ b/100-systemd-service-lesson.el @@ -0,0 +1,70 @@ +;;; 100-systemd-service-lesson.el --- systemd-service-lesson + +(defun systemd-service-lesson () + "Linux+ lesson for systemd service management." + (interactive) + (delete-other-windows) + (let ((buf (get-buffer-create "*systemd Service Lesson*")) + (shell (or shell-file-name (getenv "SHELL") "/bin/sh"))) + (switch-to-buffer buf) + (read-only-mode -1) + (erase-buffer) + (insert + "Linux+ Lesson: systemd Service Management\n" + "=========================================\n\n" + "Concept:\n" + "Learn how to manage services with systemctl and understand\n" + "the difference between starting, enabling, disabling, and masking.\n\n" + "Use BOTH VMs:\n" + "- Debian 13 usually uses service name: ssh\n" + "- Rocky Linux usually uses service name: sshd\n\n" + "Core meanings:\n" + " systemctl start = start now\n" + " systemctl stop = stop now\n" + " systemctl restart = stop then start\n" + " systemctl reload = reload config if supported\n" + " systemctl enable = start at boot\n" + " systemctl disable = do not start at boot\n" + " systemctl enable --now = start now and enable at boot\n" + " systemctl mask = prevent normal starting\n" + " systemctl unmask = remove mask\n\n" + "Hands-on tasks in Debian 13:\n" + " systemctl status ssh\n" + " systemctl is-enabled ssh\n" + " sudo systemctl restart ssh\n" + " journalctl -u ssh --since today\n\n" + "Hands-on tasks in Rocky Linux:\n" + " systemctl status sshd\n" + " systemctl is-enabled sshd\n" + " sudo systemctl restart sshd\n" + " journalctl -u sshd --since today\n\n" + "Optional lab on either disposable VM:\n" + " sudo systemctl stop \n" + " systemctl status \n" + " sudo systemctl start \n" + " sudo systemctl disable \n" + " systemctl is-enabled \n" + " sudo systemctl enable \n\n" + "Boot target review:\n" + " systemctl get-default\n" + " sudo systemctl set-default multi-user.target\n" + " sudo systemctl set-default graphical.target\n\n" + "Exam traps:\n" + "- enable does not start a service immediately.\n" + "- disable does not stop a currently running service.\n" + "- mask is stronger than disable.\n" + "- reload and restart are not the same.\n" + "- Debian commonly uses ssh; Red Hat/Rocky commonly uses sshd.\n\n" + "When finished, run:\n" + " M-x systemd-service-quiz\n") + (goto-char (point-min)) + (view-mode 1) + (split-window-below) + (other-window 1) + (let ((term (get-buffer "*term*"))) + (if (buffer-live-p term) + (switch-to-buffer term) + (ansi-term shell "term"))) + (other-window -1))) + +;;; 100-systemd-service-lesson.el ends here diff --git a/101-systemd-service-quiz.el b/101-systemd-service-quiz.el index dd74fe0..3bec06e 100644 --- a/101-systemd-service-quiz.el +++ b/101-systemd-service-quiz.el @@ -1,49 +1,764 @@ -;;; 101-systemd-service-quiz.el --- systemd-service-quiz +;;; 06-linuxplus-quiz-mode.el --- linuxplus-quiz-mode -(defun systemd-service-quiz () - "Quiz for systemd service management." +(defvar-local linuxplus-quiz-lesson-name nil) +(defvar-local linuxplus-quiz-questions nil) +(defvar-local linuxplus-quiz-user-answers nil) +(defvar-local linuxplus-quiz-current-question 0) +(defvar-local linuxplus-quiz-current-choice 0) +(defvar-local linuxplus-quiz-return-window-configuration nil) +(defvar-local linuxplus-quiz-return-buffer nil) + +(defun linuxplus-quiz--question-type (question) + (plist-get question :type)) + +(defun linuxplus-quiz--question-prompt (question) + (plist-get question :prompt)) + +(defun linuxplus-quiz--question-answer (question) + (plist-get question :answer)) + +(defun linuxplus-quiz--question-answers (question) + (plist-get question :answers)) + +(defun linuxplus-quiz--question-choices (question) + (plist-get question :choices)) + +(defun linuxplus-quiz--current-question () + (nth linuxplus-quiz-current-question linuxplus-quiz-questions)) + +(defun linuxplus-quiz--current-user-answer () + (nth linuxplus-quiz-current-question linuxplus-quiz-user-answers)) + +(defun linuxplus-quiz--set-current-user-answer (answer) + (setf (nth linuxplus-quiz-current-question + linuxplus-quiz-user-answers) + answer)) + +(defun linuxplus-quiz--answer-marker (answer) + (cond + ((eq answer 'true) "[t]") + ((eq answer 'false) "[f]") + ((eq answer 'yes) "[y]") + ((eq answer 'no) "[n]") + ((eq answer 'selected) "[*]") + (t "[]"))) + +(defun linuxplus-quiz--answer-string (answer) + (cond + ((eq answer 'true) "true") + ((eq answer 'false) "false") + ((eq answer 'yes) "yes") + ((eq answer 'no) "no") + ((listp answer) (format "%S" answer)) + ((null answer) "unanswered") + (t (format "%S" answer)))) + +(defun linuxplus-quiz--correct-answer-for-display + (question) + (let ((type + (linuxplus-quiz--question-type question))) + (cond + ((memq type '(true-false yes-no)) + (linuxplus-quiz--answer-string + (linuxplus-quiz--question-answer question))) + + ((eq type 'multiple) + (format "%S" + (linuxplus-quiz--question-answers + question))) + + (t "unknown")))) + +(defun linuxplus-quiz--advance-question () + (when (< linuxplus-quiz-current-question + (1- (length linuxplus-quiz-questions))) + (setq linuxplus-quiz-current-question + (1+ linuxplus-quiz-current-question)) + (setq linuxplus-quiz-current-choice 0))) + +(defun linuxplus-quiz--previous-question () + (when (> linuxplus-quiz-current-question 0) + (setq linuxplus-quiz-current-question + (1- linuxplus-quiz-current-question)) + (setq linuxplus-quiz-current-choice 0))) + +(defun linuxplus-quiz--multiple-selected-p + (answer choice-index) + (and (listp answer) + (memq choice-index answer))) + +(defun linuxplus-quiz--advance-choice () + "Advance to next multiple-choice bracket." + (let* ((question + (linuxplus-quiz--current-question)) + (choices + (linuxplus-quiz--question-choices + question))) + (when (and choices + (< linuxplus-quiz-current-choice + (1- (length choices)))) + (setq linuxplus-quiz-current-choice + (1+ linuxplus-quiz-current-choice))))) + +(defun linuxplus-quiz--toggle-current-choice () + "Toggle current multiple-choice selection. + +If selecting: +- add * +- advance to next bracket + +If removing: +- clear * +- stay on current bracket." + + (let* ((answer (copy-sequence + (or (linuxplus-quiz--current-user-answer) + nil))) + (choice linuxplus-quiz-current-choice) + (was-selected (memq choice answer))) + + (if was-selected + + ;; REMOVE selection and stay. + (setq answer (delq choice answer)) + + ;; ADD selection and advance. + (push choice answer) + + (setq answer (sort answer #'<)) + + (linuxplus-quiz--set-current-user-answer + answer) + + (linuxplus-quiz--advance-choice) + + ;; Return immediately. + (setq answer nil)) + + ;; Removal path only reaches here. + (when answer + (setq answer (sort answer #'<)) + + (linuxplus-quiz--set-current-user-answer + answer)))) + +(defun linuxplus-quiz--answer-correct-p + (question user-answer) + + (let ((type + (linuxplus-quiz--question-type question))) + + (cond + + ((memq type '(true-false yes-no)) + (eq user-answer + (linuxplus-quiz--question-answer + question))) + + ((eq type 'multiple) + (equal + (sort + (copy-sequence + (or user-answer nil)) + #'<) + + (sort + (copy-sequence + (linuxplus-quiz--question-answers + question)) + #'<))) + + (t nil)))) + +(defun linuxplus-quiz--question-index-at-point () + "Return zero-based question index at point." + (save-excursion + (let ((found nil)) + (when (re-search-backward + "^\\([0-9]+\\)\\.$" + nil t) + (setq found + (1- + (string-to-number + (match-string 1))))) + (when (and found + (>= found 0) + (< found + (length linuxplus-quiz-questions))) + found)))) + +(defun linuxplus-quiz--sync-current-position-from-point () + "Update current question from cursor position." + (let ((question-index + (linuxplus-quiz--question-index-at-point))) + + (when question-index + + (setq linuxplus-quiz-current-question + question-index) + + (unless + (eq + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'multiple) + + (setq linuxplus-quiz-current-choice 0))))) + +(defun linuxplus-quiz--invalid-input-message () + (linuxplus-quiz--sync-current-position-from-point) + + (let ((type + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)))) + + (cond + + ((eq type 'true-false) + (message + "Valid input: t for true, f for false.")) + + ((eq type 'yes-no) + (message + "Valid input: y for yes, n for no.")) + + ((eq type 'multiple) + (message + "Use the space bar to add/remove the * character.")) + + (t + (message "Invalid input."))))) + +(defun linuxplus-quiz--set-answer-and-maybe-advance + (answer) + + "Set ANSWER. +If unanswered, advance. +If replacing, stay." + + (linuxplus-quiz--sync-current-position-from-point) + + (let ((old-answer + (linuxplus-quiz--current-user-answer))) + + (linuxplus-quiz--set-current-user-answer + answer) + + (unless old-answer + (linuxplus-quiz--advance-question)) + + (linuxplus-quiz-render))) + +(defun linuxplus-quiz-render () + (let ((inhibit-read-only t)) + + (erase-buffer) + + (insert "Linux+ Quiz\n") + (insert "===========\n\n") + + (let ((current-type + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)))) + + (cond + + ((eq current-type 'true-false) + (insert + "Type 't' for true or 'f' for false inside the brackets.\n")) + + ((eq current-type 'yes-no) + (insert + "Type 'y' for yes or 'n' for no inside the brackets.\n")) + + ((eq current-type 'multiple) + (insert + "Use the space bar to add/remove the * character for multiple choice questions.\n")) + + (t + (insert "Answer the question.\n")))) + + (insert + "b = back, TAB = forward, x = finish, q = quit\n\n") + + (dotimes (i + (length linuxplus-quiz-questions)) + + (let* ((question + (nth i linuxplus-quiz-questions)) + + (type + (linuxplus-quiz--question-type + question)) + + (prompt + (linuxplus-quiz--question-prompt + question)) + + (user-answer + (nth i linuxplus-quiz-user-answers)) + + (current-question-p + (= i linuxplus-quiz-current-question))) + + (insert (format "%d.\n" (1+ i))) + (insert (format " %s\n" prompt)) + + (cond + + ((eq type 'true-false) + + (insert + (format + "%s %s True or False\n\n" + (if current-question-p ">" " ") + (linuxplus-quiz--answer-marker + user-answer)))) + + ((eq type 'yes-no) + + (insert + (format + "%s %s Yes or No\n\n" + (if current-question-p ">" " ") + (linuxplus-quiz--answer-marker + user-answer)))) + + ((eq type 'multiple) + + (let ((choices + (linuxplus-quiz--question-choices + question))) + + (dotimes + (choice-index + (length choices)) + + (let* + ((selected + (if + (linuxplus-quiz--multiple-selected-p + user-answer + choice-index) + 'selected + nil)) + + (choice-current-p + (and current-question-p + (= choice-index + linuxplus-quiz-current-choice)))) + + (insert + (format + "%s %s %s\n" + (if choice-current-p ">" " ") + (linuxplus-quiz--answer-marker + selected) + (nth choice-index choices)))))) + + (insert "\n"))))) + + (insert + "\nReminder: x = finish quiz, q = quit/return to lesson.\n")) + + (linuxplus-quiz-goto-current-answer)) + +(defun linuxplus-quiz-goto-current-answer () + (goto-char (point-min)) + + (let ((target-question + (format "^%d\\.$" + (1+ + linuxplus-quiz-current-question)))) + + (when + (re-search-forward + target-question + nil t) + + (let ((type + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)))) + + (cond + + ((memq type '(true-false yes-no)) + + (when + (re-search-forward + "\\[[^]]*\\]" + nil t) + + (backward-char 1))) + + ((eq type 'multiple) + + (dotimes + (_ linuxplus-quiz-current-choice) + + (re-search-forward + "\\[[^]]*\\]" + nil t)) + + (when + (re-search-forward + "\\[[^]]*\\]" + nil t) + + (backward-char 1)))))))) + +(defun linuxplus-quiz-answer-true () (interactive) - (linuxplus-start-quiz - 'systemd-service-lesson - '((:type true-false - :prompt "`systemctl start ssh` starts the service immediately but does not enable it at boot." - :answer true) - (:type true-false - :prompt "`systemctl enable ssh` starts the service immediately and enables it at boot." - :answer false) + (linuxplus-quiz--sync-current-position-from-point) - (:type true-false - :prompt "`systemctl disable ssh` prevents startup at boot but does not necessarily stop a currently running service." - :answer true) + (if + (eq + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'true-false) - (:type true-false - :prompt "`systemctl mask ssh` is stronger than `systemctl disable ssh` because it prevents normal starting." - :answer true) + (linuxplus-quiz--set-answer-and-maybe-advance + 'true) - (:type true-false - :prompt "`systemctl reload ssh` and `systemctl restart ssh` always do exactly the same thing." - :answer false) + (linuxplus-quiz--invalid-input-message))) - (:type true-false - :prompt "`systemctl status ` is useful when troubleshooting a failed service." - :answer true) +(defun linuxplus-quiz-answer-false () + (interactive) - (:type true-false - :prompt "`journalctl -u ` can show logs for a specific systemd service." - :answer true) + (linuxplus-quiz--sync-current-position-from-point) - (:type true-false - :prompt "`systemctl --failed` can help identify failed units." - :answer true) + (if + (eq + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'true-false) - (:type yes-no - :prompt "On many Red Hat-family systems, is the SSH service commonly named `sshd`?" - :answer yes) + (linuxplus-quiz--set-answer-and-maybe-advance + 'false) - (:type yes-no - :prompt "On many Debian-family systems, is the SSH service commonly named `sshd` instead of `ssh`?" - :answer no)) - "*systemd Service Quiz*")) + (linuxplus-quiz--invalid-input-message))) -;;; 101-systemd-service-quiz.el ends here +(defun linuxplus-quiz-answer-yes () + (interactive) + + (linuxplus-quiz--sync-current-position-from-point) + + (if + (eq + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'yes-no) + + (linuxplus-quiz--set-answer-and-maybe-advance + 'yes) + + (linuxplus-quiz--invalid-input-message))) + +(defun linuxplus-quiz-answer-no () + (interactive) + + (linuxplus-quiz--sync-current-position-from-point) + + (if + (eq + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'yes-no) + + (linuxplus-quiz--set-answer-and-maybe-advance + 'no) + + (linuxplus-quiz--invalid-input-message))) + +(defun linuxplus-quiz-toggle-multiple () + (interactive) + + (linuxplus-quiz--sync-current-position-from-point) + + (if + (eq + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)) + 'multiple) + + (progn + (linuxplus-quiz--toggle-current-choice) + (linuxplus-quiz-render)) + + (linuxplus-quiz--invalid-input-message))) + +(defun linuxplus-quiz-next () + (interactive) + + (linuxplus-quiz--sync-current-position-from-point) + + (let ((type + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)))) + + (if + (and + (eq type 'multiple) + + (< linuxplus-quiz-current-choice + (1- + (length + (linuxplus-quiz--question-choices + (linuxplus-quiz--current-question)))))) + + (setq linuxplus-quiz-current-choice + (1+ + linuxplus-quiz-current-choice)) + + (linuxplus-quiz--advance-question))) + + (linuxplus-quiz-render)) + +(defun linuxplus-quiz-prev () + (interactive) + + (linuxplus-quiz--sync-current-position-from-point) + + (let ((type + (linuxplus-quiz--question-type + (linuxplus-quiz--current-question)))) + + (if + (and + (eq type 'multiple) + (> linuxplus-quiz-current-choice 0)) + + (setq linuxplus-quiz-current-choice + (1- + linuxplus-quiz-current-choice)) + + (linuxplus-quiz--previous-question))) + + (linuxplus-quiz-render)) + +(defun linuxplus-quiz-invalid-key () + (interactive) + (linuxplus-quiz--invalid-input-message)) + +(defun linuxplus-quiz-return-to-lesson () + "Return to lesson/window layout." + (interactive) + + (let ((config + linuxplus-quiz-return-window-configuration) + + (buffer + linuxplus-quiz-return-buffer)) + + (when + (window-configuration-p config) + + (set-window-configuration config)) + + (when + (buffer-live-p buffer) + + (switch-to-buffer buffer)))) + +(defun linuxplus-quiz-finish () + (interactive) + + (let ((score 0) + + (total + (length linuxplus-quiz-questions))) + + (dotimes (i total) + + (when + (linuxplus-quiz--answer-correct-p + (nth i linuxplus-quiz-questions) + (nth i linuxplus-quiz-user-answers)) + + (setq score (1+ score)))) + + (linuxplus-record-score + linuxplus-quiz-lesson-name + score) + + (let ((inhibit-read-only t)) + + (erase-buffer) + + (insert "Linux+ Quiz Results\n") + (insert "===================\n\n") + + (insert + (format + "Lesson: %s\n" + linuxplus-quiz-lesson-name)) + + (insert + (format + "Score: %d/%d\n\n" + score total)) + + (insert "Detailed review:\n\n") + + (dotimes (i total) + + (let* + ((question + (nth i linuxplus-quiz-questions)) + + (prompt + (linuxplus-quiz--question-prompt + question)) + + (user-answer + (nth i linuxplus-quiz-user-answers))) + + (insert + (format + "%d. %s\n" + (1+ i) + prompt)) + + (insert + (format + " Your answer: %s\n" + (linuxplus-quiz--answer-string + user-answer))) + + (insert + (format + " Correct answer: %s\n" + (linuxplus-quiz--correct-answer-for-display + question))) + + (insert + (format + " Result: %s\n\n" + + (if + (linuxplus-quiz--answer-correct-p + question + user-answer) + + "correct" + + "incorrect"))))) + + (insert + "Press q to return to the lesson.\n")) + + (goto-char (point-min)) + (linuxplus-quiz-mode) + + (message + "Quiz complete. Score: %d/%d" + score total))) + +(setq linuxplus-quiz-mode-map + + (let ((map + (make-sparse-keymap))) + + ;; invalid printable input + + (dotimes (i 95) + + (define-key + map + (char-to-string (+ i 32)) + #'linuxplus-quiz-invalid-key)) + + ;; valid answers + + (define-key map + (kbd "t") + #'linuxplus-quiz-answer-true) + + (define-key map + (kbd "f") + #'linuxplus-quiz-answer-false) + + (define-key map + (kbd "y") + #'linuxplus-quiz-answer-yes) + + (define-key map + (kbd "n") + #'linuxplus-quiz-answer-no) + + (define-key map + (kbd "SPC") + #'linuxplus-quiz-toggle-multiple) + + ;; navigation + + (define-key map + (kbd "b") + #'linuxplus-quiz-prev) + + (define-key map + (kbd "TAB") + #'linuxplus-quiz-next) + + (define-key map + (kbd "x") + #'linuxplus-quiz-finish) + + (define-key map + (kbd "q") + #'linuxplus-quiz-return-to-lesson) + + map)) + +(define-derived-mode + linuxplus-quiz-mode + special-mode + "LinuxPlus-Quiz" + "Major mode for Linux+ quizzes." + + (setq buffer-read-only t)) + +(defun linuxplus-start-quiz + (lesson questions buffer) + + "Start Linux+ quiz in full window." + + (let ((return-config + (current-window-configuration)) + + (return-buffer + (current-buffer)) + + (buf + (get-buffer-create buffer))) + + (delete-other-windows) + + (switch-to-buffer buf) + + (linuxplus-quiz-mode) + + (setq linuxplus-quiz-return-window-configuration + return-config) + + (setq linuxplus-quiz-return-buffer + return-buffer) + + (setq linuxplus-quiz-lesson-name + lesson) + + (setq linuxplus-quiz-questions + questions) + + (setq linuxplus-quiz-user-answers + (make-list + (length questions) + nil)) + + (setq linuxplus-quiz-current-question 0) + (setq linuxplus-quiz-current-choice 0) + + (linuxplus-quiz-render))) + +;;; 06-linuxplus-quiz-mode.el ends here diff --git a/102-user-management-lesson.el b/102-user-management-lesson.el new file mode 100644 index 0000000..8d75c4c --- /dev/null +++ b/102-user-management-lesson.el @@ -0,0 +1,144 @@ +(defun linuxplus-user-management-lesson () + "CompTIA Linux+ lesson: user management with lesson window and ansi-term." + (interactive) + + (let ((lesson-buffer (get-buffer-create "*Linux+ User Management Lesson*")) + (term-buffer-name "*Linux+ Terminal*") + (shell-program (or shell-file-name "/bin/bash"))) + + ;; Prepare lesson buffer + (switch-to-buffer lesson-buffer) + (read-only-mode -1) + (erase-buffer) + + (insert +"Linux+ XK0-005 Lesson: User and Group Management + +Study objectives: +- Linux account files +- User defaults +- Group management +- Account troubleshooting + + +================================================== +/etc/passwd +================================================== + +Stores: +- username +- UID +- primary GID +- home directory +- shell + +Does NOT store: +- password hashes +- password aging + + +================================================== +/etc/shadow +================================================== + +Stores: +- password hashes +- expiration +- password aging + + +Commands: + +sudo chage -l $USER + + +================================================== +/etc/group +================================================== + +Stores group information. + +Commands: + +groups $USER + +id $USER + + +================================================== +User defaults +================================================== + +Files: + +/etc/default/useradd +/etc/login.defs + + +Try: + +cat /etc/default/useradd + +grep UID /etc/login.defs + + +================================================== +usermod exam trap +================================================== + + +SAFE: + +sudo usermod -aG GROUP USER + + +DANGEROUS: + +sudo usermod -G GROUP USER + + +Without -a: +Existing supplementary groups are replaced. + + +================================================== +Rename groups +================================================== + +Syntax: + +groupmod -n NEW OLD + + +Example: + +sudo groupmod -n Development WIP + + +================================================== + +When finished: + +M-x linuxplus-user-management-quiz + +") + + (goto-char (point-min)) + + ;; Create the study layout + (delete-other-windows) + + ;; Top = lesson + ;; Bottom = terminal + + (split-window-below) + + (other-window 1) + + ;; Reuse terminal if already created + (if (get-buffer term-buffer-name) + (switch-to-buffer term-buffer-name) + (ansi-term shell-program term-buffer-name)) + + ;; Put cursor back in lesson + (other-window -1))) diff --git a/103-user-management-quiz.el b/103-user-management-quiz.el new file mode 100644 index 0000000..b855401 --- /dev/null +++ b/103-user-management-quiz.el @@ -0,0 +1,52 @@ +;;; 103-user-management-quiz.el --- user-management-quiz + +(defun linuxplus-user-management-quiz () + "Quiz for Linux+ user and group management." + (interactive) + + (linuxplus-start-quiz + 'linuxplus-user-management-lesson + + '((:type true-false + :prompt "The /etc/passwd file stores password expiration and aging information." + :answer false) + + (:type true-false + :prompt "The /etc/group and /etc/passwd files are useful when troubleshooting group membership issues." + :answer true) + + (:type true-false + :prompt "Running usermod -G developers bob always preserves existing supplementary groups." + :answer false) + + (:type true-false + :prompt "The command usermod -aG wheel bob appends bob to the wheel group." + :answer true) + + (:type true-false + :prompt "The file /etc/login.defs can define UID ranges and password aging defaults." + :answer true) + + (:type true-false + :prompt "The command groupmod -n NewName OldName renames a Linux group." + :answer true) + + (:type true-false + :prompt "The x shown in /etc/passwd means the account has no password." + :answer false) + + (:type true-false + :prompt "The id command can display UID, GID, and group memberships." + :answer true) + + (:type yes-no + :prompt "Is a mobile authenticator generating one-time passwords an example of a software token?" + :answer yes) + + (:type yes-no + :prompt "Should /etc/profile be checked first when verifying UID and group membership problems?" + :answer no)) + + "*Linux+ User Management Quiz*")) + +;;; 103-user-management-quiz.el ends here diff --git a/61-file-permissions-quiz.el b/61-file-permissions-quiz.el new file mode 100644 index 0000000..bb49ef5 --- /dev/null +++ b/61-file-permissions-quiz.el @@ -0,0 +1,16 @@ +;;; 61-file-permissions-quiz.el --- file-permissions-quiz + +(defun file-permissions-quiz () + "Quiz for Linux file permissions and ownership." + (interactive) + (linuxplus-start-quiz + 'file-permissions-lesson + '("Does chmod change file permissions?" + "Does chown change ownership?" + "Does 755 equal rwxr-xr-x?" + "Does 644 equal rw-r--r--?" + "Does execute permission on a directory allow traversal?") + '(y y y y y) + "*File Permissions Quiz*")) + +;;; 61-file-permissions-quiz.el ends here diff --git a/70-process-management-lesson.el b/70-process-management-lesson.el new file mode 100644 index 0000000..2f010a9 --- /dev/null +++ b/70-process-management-lesson.el @@ -0,0 +1,63 @@ +;;; 70-process-management-lesson.el --- process-management-lesson + +(defun process-management-lesson () + "Linux+ lesson for process management and signals." + (interactive) + (delete-other-windows) + (let ((buf (get-buffer-create "*Process Management Lesson*")) + (shell (or shell-file-name (getenv "SHELL") "/bin/sh"))) + (switch-to-buffer buf) + (read-only-mode -1) + (erase-buffer) + (insert + "Linux+ Lesson: Process Management and Signals\n" + "============================================\n\n" + "Concepts:\n" + "- ps shows processes\n" + "- top shows live processes\n" + "- kill sends signals to processes\n" + "- pkill kills by process name\n" + "- jobs shows shell background jobs\n\n" + "Important signals:\n" + " SIGTERM = 15 = graceful termination\n" + " SIGKILL = 9 = forced termination\n\n" + "Hands-on tasks:\n\n" + "1. View running processes:\n" + " ps aux | less\n\n" + "2. View live processes:\n" + " top\n" + " Press q to quit.\n\n" + "3. Start a background process:\n" + " sleep 300 &\n\n" + "4. View background jobs:\n" + " jobs\n\n" + "5. Find the sleep process:\n" + " ps aux | grep sleep\n\n" + "6. Kill the process gracefully:\n" + " kill PID\n\n" + "7. Start another sleep process:\n" + " sleep 300 &\n\n" + "8. Kill it by name:\n" + " pkill sleep\n\n" + "9. Start another sleep process:\n" + " sleep 300 &\n\n" + "10. Kill with SIGKILL:\n" + " kill -9 PID\n\n" + "Linux+ exam traps:\n" + "- kill sends signals, it does NOT always mean SIGKILL\n" + "- kill default signal is SIGTERM (15)\n" + "- pkill works by process name\n" + "- ps aux is VERY important\n\n" + "When finished, run:\n" + " M-x process-management-quiz\n") + (goto-char (point-min)) + (view-mode 1) + (split-window-below) + (other-window 1) + (let ((term (get-buffer "*term*"))) + (if (buffer-live-p term) + (switch-to-buffer term) + (ansi-term shell "term"))) + (other-window -1))) + +;;; 70-process-management-lesson.el ends here diff --git a/71-process-management-quiz.el b/71-process-management-quiz.el new file mode 100644 index 0000000..e4244a2 --- /dev/null +++ b/71-process-management-quiz.el @@ -0,0 +1,16 @@ +;;; 71-process-management-quiz.el --- process-management-quiz + +(defun process-management-quiz () + "Quiz for Linux process management and signals." + (interactive) + (linuxplus-start-quiz + 'process-management-lesson + '("Does ps display processes?" + "Is SIGKILL signal 9?" + "Does kill default to SIGTERM?" + "Does pkill kill by process name?" + "Does top show live process activity?") + '(y y y y y) + "*Process Management Quiz*")) + +;;; 71-process-management-quiz.el ends here diff --git a/80-log-filtering-lesson.el b/80-log-filtering-lesson.el new file mode 100644 index 0000000..408de75 --- /dev/null +++ b/80-log-filtering-lesson.el @@ -0,0 +1,66 @@ +;;; 80-log-filtering-lesson.el --- log-filtering-lesson + +(defun log-filtering-lesson () + "Linux+ lesson for log filtering and grep chaining." + (interactive) + (delete-other-windows) + (let ((buf (get-buffer-create "*Log Filtering Lesson*")) + (shell (or shell-file-name (getenv "SHELL") "/bin/sh"))) + (switch-to-buffer buf) + (read-only-mode -1) + (erase-buffer) + (insert + "Linux+ Lesson: Log Filtering and grep Chaining\n" + "==============================================\n\n" + "Concepts:\n" + "- grep searches for patterns\n" + "- pipes combine filters\n" + "- chaining grep narrows results\n" + "- logs contain noise and useful signals\n\n" + "VERY IMPORTANT:\n" + "This is WRONG:\n\n" + " grep ssh fail logfile\n\n" + "Why?\n" + "- grep expects ONE pattern\n" + "- 'fail' becomes a filename\n\n" + "Correct approaches:\n\n" + " grep ssh logfile | grep fail\n\n" + "or:\n\n" + " grep 'ssh.*fail\\|fail.*ssh' logfile\n\n" + "Hands-on tasks:\n\n" + "1. View recent journal entries:\n" + " journalctl | head\n\n" + "2. Search for ssh activity:\n" + " journalctl | grep -i ssh\n\n" + "3. Search for failures:\n" + " journalctl | grep -i fail\n\n" + "4. Chain filters together:\n" + " journalctl | grep -i ssh | grep -i fail\n\n" + "5. Search for invalid users:\n" + " journalctl | grep -i invalid\n\n" + "6. Search for authentication issues:\n" + " journalctl | grep -i denied\n\n" + "7. Compare broad vs narrow filtering:\n" + " journalctl | grep -i ssh\n" + " journalctl | grep -i ssh | grep -i fail\n\n" + "What to notice:\n" + "- first command = noise\n" + "- second command = signal\n" + "- chained grep reduces output size\n\n" + "Linux+ exam traps:\n" + "- grep takes ONE pattern unless combined explicitly\n" + "- grep chaining is very common\n" + "- logs are often filtered progressively\n\n" + "When finished, run:\n" + " M-x log-filtering-quiz\n") + (goto-char (point-min)) + (view-mode 1) + (split-window-below) + (other-window 1) + (let ((term (get-buffer "*term*"))) + (if (buffer-live-p term) + (switch-to-buffer term) + (ansi-term shell "term"))) + (other-window -1))) + +;;; 80-log-filtering-lesson.el ends here diff --git a/81-log-filtering-quiz.el b/81-log-filtering-quiz.el new file mode 100644 index 0000000..df48217 --- /dev/null +++ b/81-log-filtering-quiz.el @@ -0,0 +1,16 @@ +;;; 81-log-filtering-quiz.el --- log-filtering-quiz + +(defun log-filtering-quiz () + "Quiz for log filtering and grep chaining." + (interactive) + (linuxplus-start-quiz + 'log-filtering-lesson + '("Does grep normally take one pattern?" + "Does grep ssh fail logfile treat fail as a filename?" + "Can grep chaining reduce noisy output?" + "Does grep -i ignore case?" + "Is grep ssh logfile | grep fail a valid filter chain?") + '(y y y y y) + "*Log Filtering Quiz*")) + +;;; 81-log-filtering-quiz.el ends here diff --git a/90-file-search-lesson.el b/90-file-search-lesson.el new file mode 100644 index 0000000..4b67a8c --- /dev/null +++ b/90-file-search-lesson.el @@ -0,0 +1,83 @@ +;;; 90-file-search-lesson.el --- file-search-lesson + +(defun file-search-lesson () + "Linux+ lesson for finding files and commands." + (interactive) + (delete-other-windows) + (let ((buf (get-buffer-create "*File Search Lesson*")) + (shell (or shell-file-name (getenv "SHELL") "/bin/sh"))) + (switch-to-buffer buf) + (read-only-mode -1) + (erase-buffer) + + (insert + "Linux+ Lesson: File Searching and Command Discovery\n" + "==================================================\n\n" + + "Concepts:\n" + "- `find` searches the filesystem in real time\n" + "- `locate` searches a database\n" + "- `which` finds executable paths\n" + "- `grep` filters output\n\n" + + "VERY IMPORTANT:\n" + "- `find` is slower but current\n" + "- `locate` is faster but may be outdated\n\n" + + "Hands-on tasks:\n\n" + + "1. Create a practice directory:\n" + " mkdir -p ~/linuxplus-search-test\n" + " cd ~/linuxplus-search-test\n\n" + + "2. Create practice files:\n" + " touch alpha.txt beta.log gamma.conf\n\n" + + "3. Find a specific file:\n" + " find . -name alpha.txt\n\n" + + "4. Find all .txt files:\n" + " find . -name '*.txt'\n\n" + + "5. Find directories only:\n" + " find . -type d\n\n" + + "6. Find files only:\n" + " find . -type f\n\n" + + "7. Search for a command location:\n" + " which grep\n" + " which systemctl\n\n" + + "8. Use locate:\n" + " locate passwd\n\n" + + "9. If locate gives poor results:\n" + " sudo updatedb\n\n" + + "10. Combine find with grep:\n" + " find /etc -name '*.conf' | grep ssh\n\n" + + "Linux+ exam traps:\n" + "- `find` searches live filesystem data\n" + "- `locate` uses a database\n" + "- `which` searches PATH for executables\n" + "- `find` is commonly combined with pipes\n\n" + + "When finished, run:\n" + " M-x file-search-quiz\n") + + (goto-char (point-min)) + (view-mode 1) + + (split-window-below) + (other-window 1) + + (let ((term (get-buffer "*term*"))) + (if (buffer-live-p term) + (switch-to-buffer term) + (ansi-term shell "term"))) + + (other-window -1))) + +;;; 90-file-search-lesson.el ends here diff --git a/91-file-search-quiz.el b/91-file-search-quiz.el new file mode 100644 index 0000000..00cb7a5 --- /dev/null +++ b/91-file-search-quiz.el @@ -0,0 +1,50 @@ +;;; 91-file-search-quiz.el --- file-search-quiz + +(defun file-search-quiz () + "Quiz for Linux file searching tools." + (interactive) + (linuxplus-start-quiz + 'file-search-lesson + '((:type true-false + :prompt "`find` searches the live filesystem while `locate` uses a database." + :answer 'true) + + (:type true-false + :prompt "`which grep` can display the path to the `grep` executable." + :answer 'true) + + (:type true-false + :prompt "`locate` always reflects the most current filesystem state." + :answer 'false) + + (:type true-false + :prompt "`find . -type f` searches only for files." + :answer 'true) + + (:type true-false + :prompt "`find /etc -name '*.conf' | grep ssh` is a valid filter chain." + :answer 'true) + + (:type multiple + :prompt "Which statements about Linux file-search tools are correct?" + :choices ("`find` searches the live filesystem." + "`locate` uses a database." + "`which` searches PATH for executables." + "All of the above.") + :answers (0 1 2 3)) + + (:type multiple + :prompt "Which statements are false?" + :choices ("`locate` can show stale results if its database is outdated." + "`which` searches the entire filesystem." + "`find` requires an `updatedb` database before it works." + "`find . -name '*.txt'` can search for text files below the current directory.") + :answers (1 2)) + + (:type yes-no + :prompt "Does `updatedb` refresh the database used by `locate`?" + :answer 'yes)) + + "*File Search Quiz*")) + +;;; 91-file-search-quiz.el ends here