print()
return
ui = _cli.CommandLineInterface(quiz=quiz, answers=answers, stack=stack)
- ui.run()
- ui.answers.save()
+ try:
+ ui.run()
+ finally:
+ ui.answers.save()
ui.display_results()
# quizzer. If not, see <http://www.gnu.org/licenses/>.
import logging as _logging
-import os.path as _os_path
-import tempfile as _tempfile
+import os as _os
from . import error as _error
from . import util as _util
class ScriptQuestion (Question):
+ """Question testing scripting knowledge
+
+ Or using a script interpreter (like the POSIX shell) to test some
+ other knowledge.
+
+ If stdout/stderr capture is acceptable (e.g. if you're only
+ running non-interactive commands or curses applications that grab
+ the TTY directly), you can just run `.check()` like a normal
+ question.
+
+ If, on the other hand, you want users to be able to interact with
+ stdout and stderr (e.g. to drop into a shell in the temporary
+ directory), use:
+
+ tempdir = q.setup_tempdir()
+ try:
+ tempdir.invoke(..., env=q.get_environment())
+ # can call .invoke() multiple times here
+ self.check(tempdir=tempdir) # optional answer argument
+ finally:
+ tempdir.cleanup() # occasionally redundant, but that's ok
+ """
_state_attributes = Question._state_attributes + [
'interpreter',
'setup',
+ 'pre_answer',
+ 'post_answer',
'teardown',
+ 'environment',
+ 'allow_interactive',
+ 'compare_answers',
'timeout',
]
state['interpreter'] = 'sh' # POSIX-compatible shell
if 'timeout' not in state:
state['timeout'] = 3
- for key in ['setup', 'teardown']:
+ if 'environment' not in state:
+ state['environment'] = {}
+ for key in ['allow_interactive', 'compare_answers']:
+ if key not in state:
+ state[key] = False
+ for key in ['setup', 'pre_answer', 'post_answer', 'teardown']:
if key not in state:
state[key] = []
super(ScriptQuestion, self).__setstate__(state)
- def check(self, answer):
+ def run(self, tempdir, lines, **kwargs):
+ text = '\n'.join(lines + [''])
+ try:
+ status,stdout,stderr = tempdir.invoke(
+ interpreter=self.interpreter, text=text,
+ timeout=self.timeout, **kwargs)
+ except:
+ tempdir.cleanup()
+ raise
+ else:
+ return (status, stdout, stderr)
+
+ def setup_tempdir(self):
+ tempdir = _util.TemporaryDirectory()
+ self.run(tempdir=tempdir, lines=self.setup)
+ return tempdir
+
+ def teardown_tempdir(self, tempdir):
+ return self.run(tempdir=tempdir, lines=self.teardown)
+
+ def get_environment(self):
+ if self.environment:
+ env = {}
+ env.update(_os.environ)
+ env.update(self.environment)
+ return env
+
+ def _invoke(self, answer=None, tempdir=None):
+ """Run the setup/answer/teardown process
+
+ If tempdir is not None, skip the setup process.
+ If answer is None, skip the answer process.
+
+ In any case, cleanup the tempdir before returning.
+ """
+ if not tempdir:
+ tempdir = self.setup_tempdir()
+ try:
+ if answer:
+ if not self.multiline:
+ answer = [answer]
+ a_status,a_stdout,a_stderr = self.run(
+ tempdir=tempdir,
+ lines=self.pre_answer + answer + self.post_answer,
+ env=self.get_environment())
+ else:
+ a_status = a_stdout = a_stderr = None
+ t_status,t_stdout,t_stderr = self.teardown_tempdir(tempdir=tempdir)
+ finally:
+ tempdir.cleanup()
+ return (a_status,a_stdout,a_stderr,
+ t_status,t_stdout,t_stderr)
+
+ def check(self, answer=None, tempdir=None):
+ """Compare the user's answer with expected values
+
+ Arguments are passed through to ._invoke() for calculating the
+ user's response.
+ """
# figure out the expected values
- e_status,e_stdout,e_stderr = self._invoke(self.answer)
+ (ea_status,ea_stdout,ea_stderr,
+ et_status,et_stdout,et_stderr) = self._invoke(answer=self.answer)
# get values for the user-supplied answer
try:
- a_status,a_stdout,a_stderr = self._invoke(answer)
- except _error.CommandError as e:
- LOG.warning(e)
+ (ua_status,ua_stdout,ua_stderr,
+ ut_status,ut_stdout,ut_stderr) = self._invoke(
+ answer=answer, tempdir=tempdir)
+ except (KeyboardInterrupt, _error.CommandError) as e:
+ if isinstance(e, KeyboardInterrupt):
+ LOG.warning('KeyboardInterrupt')
+ else:
+ LOG.warning(e)
+ return False
+ # compare user-generated output with expected values
+ if answer:
+ if self.compare_answers:
+ if _util.invocation_difference( # compare answers
+ ea_status, ea_stdout, ea_stderr,
+ ua_status, ua_stdout, ua_stderr):
+ return False
+ elif ua_stderr:
+ LOG.warning(ua_stderr)
+ if _util.invocation_difference( # compare teardown
+ et_status, et_stdout, et_stderr,
+ ut_status, ut_stdout, ut_stderr):
return False
- for (name, e, a) in [
- ('stderr', e_stderr, a_stderr),
- ('status', e_status, a_status),
- ('stdout', e_stdout, a_stdout),
- ]:
- if a != e:
- if name == 'status':
- LOG.info(
- 'missmatched {}, expected {!r} but got {!r}'.format(
- name, e, a))
- else:
- LOG.info('missmatched {}, expected:'.format(name))
- LOG.info(e)
- LOG.info('but got:')
- LOG.info(a)
- return False
return True
-
- def _invoke(self, answer):
- prefix = '{}-'.format(type(self).__name__)
- if not self.multiline:
- answer = [answer]
- script = '\n'.join(self.setup + answer + self.teardown + [''])
- with _tempfile.NamedTemporaryFile(
- mode='w', prefix='{}script-'.format(prefix)) as tempscript:
- tempscript.write(script)
- tempscript.flush()
- with _tempfile.TemporaryDirectory(prefix=prefix) as tempdir:
- status,stdout,stderr = _util.invoke(
- args=[self.interpreter, tempscript.name],
- cwd=tempdir,
- universal_newlines=True,
- timeout=self.timeout,
- )
- dirname = _os_path.basename(tempdir)
- stdout = stdout.replace(dirname, '{}XXXXXX'.format(prefix))
- stderr = stderr.replace(dirname, '{}XXXXXX'.format(prefix))
- return status,stdout,stderr
+
for name,obj in list(locals().items()):
if name.startswith('_'):
if self.stack:
return self.stack.pop(0)
- def process_answer(self, question, answer):
- correct = question.check(answer)
+ def process_answer(self, question, answer, **kwargs):
+ correct = question.check(answer=answer, **kwargs)
self.answers.add(question=question, answer=answer, correct=correct)
if not correct:
self.stack.insert(0, question)
return text
print(e)
+from .. import error as _error
from . import UserInterface
self.ui = ui
if self.ui.quiz.introduction:
self.intro = '\n\n'.join([self.intro, self.ui.quiz.introduction])
+ self._tempdir = None
def get_question(self):
self.question = self.ui.get_question()
def _reset(self):
self.answers = []
+ if self._tempdir:
+ self._tempdir.cleanup() # occasionally redundant, but that's ok
+ self._tempdir = None
self._set_ps1()
def _set_ps1(self):
answer = self.answers[0]
else:
answer = ''
- correct = self.ui.process_answer(question=self.question, answer=answer)
+ kwargs = {}
+ if self._tempdir:
+ kwargs['tempdir'] = self._tempdir
+ correct = self.ui.process_answer(
+ question=self.question, answer=answer, **kwargs)
if correct:
print(_colorize(self.ui.colors['correct'], 'correct\n'))
else:
"""
return self.default(arg)
+ def do_shell(self, arg):
+ """Run a shell command in the question temporary directory
+
+ For example, you can spawn an interactive session with:
+
+ quizzer? !bash
+
+ If the question does not allow interactive sessions, this
+ action is a no-op.
+ """
+ if getattr(self.question, 'allow_interactive', False):
+ if not self._tempdir:
+ self._tempdir = self.question.setup_tempdir()
+ try:
+ self._tempdir.invoke(
+ interpreter='/bin/sh', text=arg, stdout=None, stderr=None,
+ universal_newlines=False,
+ env=self.question.get_environment())
+ except (KeyboardInterrupt, _error.CommandError) as e:
+ if isinstance(e, KeyboardInterrupt):
+ LOG.warning('KeyboardInterrupt')
+ else:
+ LOG.warning(e)
+ self._tempdir.cleanup()
+ self._tempdir = None
+
def do_quit(self, arg):
"Stop taking the quiz"
self._reset()
# You should have received a copy of the GNU General Public License along with
# quizzer. If not, see <http://www.gnu.org/licenses/>.
+import logging as _logging
+import os.path as _os_path
import subprocess as _subprocess
+import tempfile as _tempfile
from . import error as _error
-def invoke(args, stdin=None, universal_newlines=False, timeout=None,
- expect=None, **kwargs):
+LOG = _logging.getLogger(__name__)
+
+
+def invoke(args, stdin=None, stdout=_subprocess.PIPE, stderr=_subprocess.PIPE,
+ universal_newlines=False, timeout=None, expect=None, **kwargs):
if stdin:
stdin_pipe = _subprocess.PIPE
else:
stdin_pipe = None
try:
p = _subprocess.Popen(
- args, stdin=stdin_pipe, stdout=_subprocess.PIPE,
- stderr=_subprocess.PIPE, universal_newlines=universal_newlines,
- **kwargs)
+ args, stdin=stdin_pipe, stdout=stdout, stderr=stderr,
+ universal_newlines=universal_newlines, **kwargs)
except FileNotFoundError as e:
raise _error.CommandError(arguments=args, stdin=stdin) from e
try:
args=args, stdin=stdin, stdout=stdout, stderr=stderr,
status=status)
return (status, stdout, stderr)
+
+def invocation_difference(a_status, a_stdout, a_stderr,
+ b_status, b_stdout, b_stderr):
+ for (name, a, b) in [
+ ('stderr', a_stderr, b_stderr),
+ ('status', a_status, b_status),
+ ('stdout', a_stdout, b_stdout),
+ ]:
+ if a != b:
+ LOG.info(format_invocation_difference(name=name, a=a, b=b))
+ return (name, a, b)
+
+def format_invocation_difference(name, a, b):
+ if name == 'status':
+ return 'missmatched {}, expected {!r} but got {!r}'.format(name, a, b)
+ else:
+ return '\n'.join([
+ 'missmatched {}, expected:'.format(name),
+ a,
+ 'but got:',
+ b,
+ ])
+
+
+class TemporaryDirectory (object):
+ """A temporary directory for testing answers
+
+ >>> t = TemporaryDirectory()
+
+ Basic command execution:
+
+ >>> t.invoke('/bin/sh', 'touch a b c')
+ (0, '', '')
+ >>> t.invoke('/bin/sh', 'ls')
+ (0, 'a\nb\nc\n', '')
+
+ Captured stdout and stderr have instances of the random temporary
+ directory name normalized for easy comparison:
+
+ >>> t.invoke('/bin/sh', 'pwd')
+ (0, '/tmp/TemporaryDirectory-XXXXXX\n', '')
+
+ >>> t.cleanup()
+ """
+ def __init__(self):
+ self.prefix = '{}-'.format(type(self).__name__)
+ self.tempdir = _tempfile.TemporaryDirectory(prefix=self.prefix)
+
+ def cleanup(self):
+ if self.tempdir:
+ self.tempdir.cleanup()
+ self.tempdir = None
+
+ def __del__(self):
+ self.cleanup()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.cleanup()
+
+ def invoke(self, interpreter, text, universal_newlines=True, **kwargs):
+ if not self.tempdir:
+ raise RuntimeError(
+ 'cannot invoke() on a cleaned up {}'.format(
+ type(self).__name__))
+ with _tempfile.NamedTemporaryFile(
+ mode='w', prefix='{}script-'.format(self.prefix)
+ ) as tempscript:
+ tempscript.write(text)
+ tempscript.flush()
+ status,stdout,stderr = invoke(
+ args=[interpreter, tempscript.name],
+ cwd=self.tempdir.name,
+ universal_newlines=universal_newlines,
+ **kwargs)
+ dirname = _os_path.basename(self.tempdir.name)
+ if stdout:
+ stdout = stdout.replace(dirname, '{}XXXXXX'.format(self.prefix))
+ if stderr:
+ stderr = stderr.replace(dirname, '{}XXXXXX'.format(self.prefix))
+ return (status, stdout, stderr)
"class": "ScriptQuestion",
"intepreter": "sh",
"id": "git help config",
- "prompt": "Get help for Git's `config` command",
+ "prompt": "Get help for Git's `config` command.",
"answer": "git help config",
+ "compare_answers": true,
"help": "http://www.kernel.org/pub/software/scm/git/docs/git-help.html",
"tags": [
"help"
"id": "git config --global user.name",
"prompt": "Configure your user-wide name to be `A U Thor`.",
"answer": "git config --global user.name 'A U Thor'",
- "setup": [
- "export HOME=."
- ],
+ "environment": {
+ "HOME": "."
+ },
"teardown": [
"cat .gitconfig"
],
"id": "git config --global user.email",
"prompt": "Configure your user-wide email to be `author@example.com`.",
"answer": "git config --global user.email 'author@example.com'",
- "setup": [
- "export HOME=."
- ],
+ "environment": {
+ "HOME": "."
+ },
"teardown": [
"cat .gitconfig"
],
"git init"
],
"teardown": [
+ "cd my-project",
+ "git rev-parse --git-dir",
"git status"
],
"help": "http://www.kernel.org/pub/software/scm/git/docs/git-init.html",
"git add README",
"git commit -m 'Add a README'"
],
+ "environment": {
+ "GIT_AUTHOR_NAME": "A U Thor",
+ "GIT_AUTHOR_EMAIL": "author@example.com",
+ "GIT_COMMITTER_NAME": "C O Mitter",
+ "GIT_COMMITTER_EMAIL": "committer@example.com",
+ "GIT_AUTHOR_DATE": "1970-01-01T00:00:00Z",
+ "GIT_COMMITTER_DATE": "1970-01-01T00:00:00Z"
+ },
"setup": [
- "export GIT_AUTHOR_NAME='A U Thor'",
- "export GIT_AUTHOR_EMAIL=author@example.com",
- "export GIT_COMMITTER_NAME='C O Mitter'",
- "export GIT_COMMITTER_EMAIL=committer@example.com",
- "export GIT_AUTHOR_DATE=1970-01-01T00:00:00Z",
- "export GIT_COMMITTER_DATE=\"$GIT_AUTHOR_DATE\"",
"git init",
"echo 'This project is wonderful' > README"
],
"teardown": [
- "git ls-files",
+ "git log -p",
"git status"
],
"help": [
"How would you check?"
],
"answer": "git status",
+ "compare_answers": true,
"setup": [
"git init",
"echo 'This project is wonderful' > README",
],
"timeout": null,
"answer": "git commit -am 'Reformat widgets'",
+ "environment": {
+ "GIT_AUTHOR_NAME": "A U Thor",
+ "GIT_AUTHOR_EMAIL": "author@example.com",
+ "GIT_COMMITTER_NAME": "C O Mitter",
+ "GIT_COMMITTER_EMAIL": "committer@example.com",
+ "GIT_AUTHOR_DATE": "1970-01-01T00:01:00Z",
+ "GIT_COMMITTER_DATE": "1970-01-01T00:01:00Z"
+ },
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"echo 'Lots of widgets' > widgets",
"git add README widgets",
"git commit -m 'Add some widgets'",
- "export GIT_AUTHOR_DATE=1970-01-01T00:01:00Z",
- "export GIT_COMMITTER_DATE=\"$GIT_AUTHOR_DATE\"",
"echo 'Take a look in the widgets file.' >> README",
"echo 'Widget-1 should be blue' >> widgets"
],
"git rm widgets",
"git commit -am \"Remove 'widgets'\""
],
+ "environment": {
+ "GIT_AUTHOR_NAME": "A U Thor",
+ "GIT_AUTHOR_EMAIL": "author@example.com",
+ "GIT_COMMITTER_NAME": "C O Mitter",
+ "GIT_COMMITTER_EMAIL": "committer@example.com",
+ "GIT_AUTHOR_DATE": "1970-01-01T00:01:00Z",
+ "GIT_COMMITTER_DATE": "1970-01-01T00:01:00Z"
+ },
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"git init",
"echo 'Lots of widgets' > widgets",
"git add widgets",
- "git commit -m 'Add some widgets'",
- "export GIT_AUTHOR_DATE=1970-01-01T00:01:00Z",
- "export GIT_COMMITTER_DATE=\"$GIT_AUTHOR_DATE\""
+ "git commit -m 'Add some widgets'"
],
"teardown": [
"git log -p"
"Ask Git to display the changes you've make (but not staged)."
],
"answer": "git diff",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"including any changes that you may have already staged."
],
"answer": "git diff HEAD --",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"including any changes that you may have already staged."
],
"answer": "git diff HEAD -- README",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"Ask Git to display only the changes you've staged."
],
"answer": "git diff --cached",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"id": "git log",
"prompt": "Print the commits leading up to your current state.",
"answer": "git log",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"showing a patch for each commit."
],
"answer": "git log -p",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"showing the files changed by each commit."
],
"answer": "git log --stat",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"id": "git log --all",
"prompt": [
"Print every commit in your repository reachable from a reference",
- "(e.g from any tag or branch)"
+ "(e.g from any tag or branch)."
],
"answer": "git log --all",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"with each commit only using a single line."
],
"answer": "git log --oneline",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"and an ASCII-art inheritence graph."
],
"answer": "git log --oneline --graph",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"prompt": [
"Print the commits leading up to your current state,",
"with each commit only using a single line",
- "and reference (e.g. tag and branch) names before the summary"
+ "and reference (e.g. tag and branch) names before the summary."
],
"answer": "git log --oneline --decorate",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"answer": [
"git commit --amend -am 'Add a README'"
],
+ "environment": {
+ "GIT_AUTHOR_NAME": "A U Thor",
+ "GIT_AUTHOR_EMAIL": "author@example.com",
+ "GIT_COMMITTER_NAME": "C O Mitter",
+ "GIT_COMMITTER_EMAIL": "committer@example.com",
+ "GIT_AUTHOR_DATE": "1970-01-01T00:01:00Z",
+ "GIT_COMMITTER_DATE": "1970-01-01T00:01:00Z"
+ },
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"echo 'This project is terrible' > README",
"git add README",
"git commit -am 'Add a README'",
- "export GIT_AUTHOR_DATE=1970-01-01T00:01:00Z",
- "export GIT_COMMITTER_DATE=\"$GIT_AUTHOR_DATE\"",
"echo 'This project is wonderful' > README"
],
"teardown": [
"id": "git branch",
"prompt": "List all the local branches in your repository.",
"answer": "git branch",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"id": "git branch -a",
"prompt": "List all the branches (local and remote-tracking) in your repository.",
"answer": "git branch -a",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"id": "git branch -r",
"prompt": "List the remote-tracking branches in your repository.",
"answer": "git branch -r",
+ "compare_answers": true,
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"class": "ScriptQuestion",
"interpreter": "sh",
"id": "git branch -d",
- "prompt": "Delete the local `widget-x` branch",
+ "prompt": "Delete the local `widget-x` branch, which you just merged.",
"answer": "git branch -d widget-x",
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"git commit --allow-empty -m 'Dummy commit'",
"git branch widget-x"
],
+ "teardown": [
+ "git branch",
+ "git status"
+ ],
"help": "http://www.kernel.org/pub/software/scm/git/docs/git-branch.html",
"tags": [
"branch"
"class": "ScriptQuestion",
"interpreter": "sh",
"id": "git checkout",
- "prompt": "Change your working directory to the `widget-x` branch",
+ "prompt": "Change your working directory to the `widget-x` branch.",
"answer": "git checkout widget-x",
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"class": "ScriptQuestion",
"interpreter": "sh",
"id": "git checkout -b",
- "prompt": "Create and change to a new `widget-x` branch",
+ "prompt": "Create and change to a new `widget-x` branch.",
"answer": "git checkout -b widget-x",
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"class": "ScriptQuestion",
"interpreter": "sh",
"id": "git merge",
- "prompt": "Merge the `widget-x` branch into the current branch",
+ "prompt": "Merge the `widget-x` branch into the current branch.",
"answer": "git merge widget-x",
+ "environment": {
+ "GIT_AUTHOR_NAME": "A U Thor",
+ "GIT_AUTHOR_EMAIL": "author@example.com",
+ "GIT_COMMITTER_NAME": "C O Mitter",
+ "GIT_COMMITTER_EMAIL": "committer@example.com",
+ "GIT_AUTHOR_DATE": "1970-01-01T00:02:00Z",
+ "GIT_COMMITTER_DATE": "1970-01-01T00:02:00Z"
+ },
"setup": [
"export GIT_AUTHOR_NAME='A U Thor'",
"export GIT_AUTHOR_EMAIL=author@example.com",
"echo 'Widget X will be wonderful' > README",
"git add README",
"git commit -am 'Add widget-x documentation'",
- "export GIT_AUTHOR_DATE=1970-01-01T00:02:00Z",
- "export GIT_COMMITTER_DATE=\"$GIT_AUTHOR_DATE\"",
"git checkout master"
],
"teardown": [
"id": "git remote -v",
"prompt": "List your configured remotes and their associated URLs.",
"answer": "git remote -v",
+ "compare_answers": true,
"setup": [
"git init",
"git remote add alice git://alice.au/widgets.git",
"git init",
"git remote add widgets ../origin"
],
+ "pre_answer": [
+ "cd test"
+ ],
"teardown": [
- "cat .git/config"
+ "cd test",
+ "git log --all --oneline --graph --decorate"
],
"help": "http://www.kernel.org/pub/software/scm/git/docs/git-fetch.html",
"tags": [
"git add README",
"git commit -am 'Add widget-x documentation'"
],
+ "pre_answer": [
+ "cd test"
+ ],
"teardown": [
- "cd ../origin",
+ "cd origin",
"git log --oneline"
],
"help": "http://www.kernel.org/pub/software/scm/git/docs/git-push.html",
"id": "quoting spaces",
"prompt": "Call `ls` and pass it two arguments: `a` and `b c`.",
"answer": "ls a 'b c'",
+ "compare_answers": true,
"help": "http://pubs.opengroup.org/onlinepubs/009696699/utilities/xcu_chap02.html#tag_02_02"
},
{
"id": "echo constant",
"prompt": "Print the string `hello, world` to stdout.",
"answer": "echo 'hello, world'",
+ "compare_answers": true,
"help": "http://pubs.opengroup.org/onlinepubs/009696699/utilities/echo.html"
},
{
"id": "parameter expansion",
"prompt": "Print the contents of the PATH variable to stdout.",
"answer": "echo \"$PATH\"",
+ "compare_answers": true,
"help": "http://pubs.opengroup.org/onlinepubs/009696699/utilities/xcu_chap02.html#tag_02_06_02",
"dependencies": [
"echo constant"
"id": "variable assign constant",
"prompt": "Set the ABC variable to the string `xyz`.",
"answer": "ABC='xyz'",
- "teardown": [
+ "post_answer": [
"echo \"ABC: '${ABC}'\""
],
+ "compare_answers": true,
"help": "http://tldp.org/LDP/abs/html/varassignment.html"
},
{
"class": "ScriptQuestion",
"interpreter": "sh",
+ "id": "variable assign altered",
"prompt": "Prepend the string `/some/path:` to the PATH variable.",
"answer": "PATH=\"/some/path:$PATH\"",
- "teardown": [
+ "post_answer": [
"echo \"PATH: '${PATH}'\""
],
- "help": "",
+ "compare_answers": true,
+ "help": [
+ "http://pubs.opengroup.org/onlinepubs/009696699/utilities/xcu_chap02.html#tag_02_06_02",
+ "http://tldp.org/LDP/abs/html/varassignment.html"
+ ],
"dependencies": [
+ "parameter expansion",
"variable assign constant"
]
}
{
"class": "ScriptQuestion",
"interpreter": "sh",
+ "id": "ls",
"prompt": "List all the files in the current directory.",
"answer": "ls",
+ "compare_answers": true,
"setup": [
"touch file-1 file-2 file-3"
],
"interpreter": "sh",
"prompt": "Print the current directory to stdout.",
"answer": "pwd",
+ "compare_answers": true,
"help": "http://pubs.opengroup.org/onlinepubs/009696699/idx/utilities.html"
},
{
"class": "ScriptQuestion",
"interpreter": "sh",
+ "id": "cd",
"prompt": "Change to your home directory.",
"answer": "cd",
- "teardown": [
+ "post_answer": [
"pwd"
],
+ "compare_answers": true,
"help": "http://pubs.opengroup.org/onlinepubs/009696699/idx/utilities.html"
},
{
"class": "ScriptQuestion",
"interpreter": "sh",
+ "id": "cd ..",
"prompt": "Change to the parent of your current working directory.",
"answer": "cd ..",
- "teardown": [
+ "post_answer": [
"pwd"
],
+ "compare_answers": true,
"help": "http://pubs.opengroup.org/onlinepubs/009696699/idx/utilities.html",
"dependencies": [
- "change to your home directory"
+ "cd"
]
},
{
"class": "ScriptQuestion",
"interpreter": "sh",
+ "id": "cat",
"prompt": "Print the contents of README file to the terminal.",
"answer": "cat README",
+ "compare_answers": true,
"setup": [
"echo 'This project is wonderful' > README"
],
"http://pubs.opengroup.org/onlinepubs/009696699/idx/utilities.html"
],
"dependencies": [
- "list all the files in the current directory"
+ "ls"
]
}
]