{"id":16980276,"url":"https://github.com/kylebebak/questionnaire","last_synced_at":"2025-09-18T22:42:53.499Z","repository":{"id":57459355,"uuid":"56728544","full_name":"kylebebak/questionnaire","owner":"kylebebak","description":"Elegant mini-DSL for creating command line questionnaires","archived":false,"fork":false,"pushed_at":"2018-10-21T00:22:02.000Z","size":877,"stargazers_count":56,"open_issues_count":3,"forks_count":7,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-08-24T14:31:54.868Z","etag":null,"topics":["cli","pick","prompt","questionnaire","terminal"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kylebebak.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-04-20T23:55:29.000Z","updated_at":"2025-08-15T16:31:54.000Z","dependencies_parsed_at":"2022-09-10T15:41:07.877Z","dependency_job_id":null,"html_url":"https://github.com/kylebebak/questionnaire","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/kylebebak/questionnaire","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylebebak%2Fquestionnaire","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylebebak%2Fquestionnaire/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylebebak%2Fquestionnaire/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylebebak%2Fquestionnaire/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kylebebak","download_url":"https://codeload.github.com/kylebebak/questionnaire/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylebebak%2Fquestionnaire/sbom","scorecard":{"id":575190,"data":{"date":"2025-08-11","repo":{"name":"github.com/kylebebak/questionnaire","commit":"ed92642e8a2a0198da198acbcde2707f1d528585"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Code-Review","score":0,"reason":"Found 1/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 1 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-20T17:29:13.031Z","repository_id":57459355,"created_at":"2025-08-20T17:29:13.031Z","updated_at":"2025-08-20T17:29:13.031Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":275845082,"owners_count":25538995,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-18T02:00:09.552Z","response_time":77,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","pick","prompt","questionnaire","terminal"],"created_at":"2024-10-14T01:50:49.504Z","updated_at":"2025-09-18T22:42:53.414Z","avatar_url":"https://github.com/kylebebak.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# questionnaire\n\n![License](https://camo.githubusercontent.com/890acbdcb87868b382af9a4b1fac507b9659d9bf/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)\n[![PyPI version](https://badge.fury.io/py/questionnaire.svg)](https://badge.fury.io/py/questionnaire)\n[![Build Status](https://travis-ci.org/kylebebak/questionnaire.svg?branch=master)](https://travis-ci.org/kylebebak/questionnaire)\n[![Coverage Status](https://coveralls.io/repos/github/kylebebak/questionnaire/badge.svg?branch=master)](https://coveralls.io/github/kylebebak/questionnaire?branch=master)\n[![Codacy Badge](https://api.codacy.com/project/badge/Grade/bbfaa8c291834d049fac889cfdd06f4a)](https://www.codacy.com/app/kylebebak/questionnaire?utm_source=github.com\u0026amp;utm_medium=referral\u0026amp;utm_content=kylebebak/questionnaire\u0026amp;utm_campaign=Badge_Grade)\n\n__questionnaire__ is a mini-DSL for writing command line questionnaires. It prompts a user to answer a series of questions and returns the answers.\n\n\n## Features\n- Compact, intuitive syntax\n- Composable: pipe answers to other programs as JSON or plain text\n- Flexible\n  + Conditional questions can depend on previous answers\n  + Allow users to reanswer questions\n  + Validate and transform answers\n  + Question presentation decoupled from answer values\n  + [Extend questionnaire as it's being run](#github-api)\n- __choose one__, __choose many__, and __raw input__ prompters built in\n  + Extend questionnaire by writing your own prompter\n\n\n## Installation\n__questionnaire__ is written in Python. It works with Python 2 and 3, although it's a bit prettier if you run it with Python 3. The best way to install it is with `pip`.\n\n~~~sh\npip install questionnaire\n~~~\n\n\n## Getting Started\nPaste the following into a file and save it. Let's assume you save it as `questions.py`. Go to the command line and run `python questions.py`.\n\n~~~py\n# questions.py\nfrom questionnaire import Questionnaire\nq = Questionnaire()\n\nq.one('day', 'monday', 'friday', 'saturday', prompt='What day is it?')\nq.one('time', ('morning', 'in the morning'), ('night', 'at night'), prompt='What time is it?')\n\nq.run()\nprint(q.format_answers())\n~~~\n\nWhat's happening here? We instantiate a questionnaire with `q = Questionnaire()`, add two questions to it, and run the questionnaire. At the end the answers are printed to stdout as JSON.\n\nLook at the second question with the __\"time\"__ key. We pass tuples for our options instead of strings. The first value in these tuples is the answer value stored by the questionnaire, and the second value is the option presented to the user. In other words, the presentation of your questionnaire __can be decoupled from the answers it returns__.\n\n\n## Getting Fancy\nAdd a group of conditional questions to the questionnaire above. Only one of these questions will be asked, depending on the answers to the first two questions. Run the questionnaire and inspect the answers.\n\n~~~py\nq = Questionnaire()\nq.one('day', 'monday', 'friday', 'saturday')\nq.one('time', 'morning', 'night')\n\nq.many('activities', 'tacos de pastor', 'go to cantina', 'write code').condition(('time', 'night'))\nq.many('activities', 'barbacoa', 'watch footy', 'walk dog').condition(('day', 'saturday'), ('time', 'morning'))\nq.many('activities', 'eat granola', 'get dressed', 'go to work').condition(('time', 'morning'))\n\nq.run()\nprint(q.format_answers(fmt='array'))\n~~~\n\n![](https://raw.githubusercontent.com/kylebebak/questionnaire/master/examples/activities.gif)\n\nAs you can see, it's easy to print answers to stdout. In keeping with the [UNIX philosophy](https://en.wikipedia.org/wiki/Unix_philosophy), __Questionnaire is composable__. If you don't want to write Python code, you can write a standalone questionnaire that pipes its answers to another program for handling. If you want to handle them in the same script, just use `q.answers`, which is a nice `OrderedDict`.\n\nAlso, did you notice the `fmt='array'` argument? This formats the answers as a JSON array instead of a JSON object. This guarantees parsing the answers doesn't screw up their order, although it might make parsing more cumbersome.\n\n\n### Plain Text\nIf you plan on piping the results of a questionnaire to a shell script, you might want plain text instead of JSON. Just pass `fmt='plain'` when you format the answers to your `Questionnaire`. The answers will be returned as plain text, one answer per line.\n\n\n### GitHub API\nHere's another example. This one handles raw input. It first prompts the user for their GitHub __username__ and __password__. Then it pauses the questionnaire and uses these credentials to hit the GitHub API to get a list of all their repos. Finally, it adds another question to the questionnaire and resumes it, prompting the user to choose one of these repos.\n\nIt depends on the [Requests](https://github.com/requests/requests) library, so install it if you want to give it a try. First, add a question using the `raw` prompter.\n\n~~~py\nq = Questionnaire(show_answers=False, can_go_back=False)\nq.raw('user', prompt='Username:')\nq.raw('pass', prompt='Password:', secret=True)\n\nq.run()\nr = requests.get('https://api.github.com/user/repos', auth=(q.answers.get('user'), q.answers.get('pass')))\nif not(r.ok):\n    import sys\n    print('username/password incorrect')\n    sys.exit()\n\nrepos = [repo.get('url') for repo in r.json()]\nq.one('repo', *repos, prompt='Choose a repo')\nq.run()\nprint(q.answers.get('repo'))\n\n~~~\n\n\n## More Examples\nCheck out clients in the `examples` directory.\n\n\n## Prompters\nThe core prompters are currently `one`, `many`, `raw`. The first two depend on the excellent [pick](https://github.com/wong2/pick) package. All three are used in the examples above.\n\n\n### One Option\nTo require the user to pick one option from a list, invoke `questionnaire.one`. When the question is answered the chosen option is added to the `answers` dict. Pass `idx` to choose the index of the initially selected option.\n\n\n### Many Options\nTo allow the user to pick many options for a single question, invoke `questionnaire.many`. When the question is answered, the list of chosen options is added to the `answers` dict. As with the `one` prompter, users can use \u003ckbd\u003e\u0026larr;\u003c/kbd\u003e or \u003ckbd\u003eh\u003c/kbd\u003e to go back. Pass a `default` index or list of indices to specify initially selected options.\n\n\n### Raw Input\nFor raw input, invoke `questionnaire.raw`. Optionally pass a `type` (`int`, `float`, ...) to coerce the answer to a given type. You can also pass a `default` value to be inserted if the user does not write anything. By default, the user can go back from a raw input question by entering `\u003c` as the answer. To change this, pass your own `go_back` string.\n\nIf you want to capture password input, or any other secret input, pass `secret=True`.\n\n\n## Validating and Transforming Answers\n__questionnaire__ makes it easy to validate and transform answers. For validation, you chain a call to `validate` onto a question and pass a validation function. When the user answers, the validation function receives one argument (the answer).\n\nIf there's anything wrong with the answer, the function __should return a string explaining what's wrong__. The explanation is shown to the user when he submits an invalid answer. If there's nothing wrong with the answer, the function shouldn't return anything.\n\n__Transforming__ answers is very similar. Chain a call to `transform` onto a question and pass a transform function. This function receives the answer as an argument. It should do something with it and return a transformed answer. The transformed answer is the one that will actually be saved in the questionnaire. See how you can use __questionnaire__ to help your users sign up for junk mail.\n\n~~~py\nq = Questionnaire(can_go_back=False)\n\ndef email(email):\n    import re\n    if not re.match(r'[^@]+@[^@]+\\.[^@]+', email):\n        return 'Enter a valid email'\n\ndef one(options):\n    if len(options) \u003c 1:\n        return 'You must choose at least 1 type of junk mail'\n\ndef join(options):\n    return ', '.join(options)\n\nq.raw('email').validate(email)\nq.many('junk_mail', 'this one weird trick', 'cheap viagra', 'dermatologists hate her').validate(one).transform(join)\n\nprint(q.run())\n~~~\n\nIf a question has both a `transform` and `validate` function, validation is performed on the answer __before__ the transform is applied.\n\n\n## Conditional Questions\nOne of __questionnaire__'s coolest features is asking questions conditionally based on previous answers. The API for conditional questions is simple and flexible.\n\nIf you add questions with the same key to a questionnaire, the conditions assigned to the questions determine which one is presented to the user. __questionnaire__ iterates through the questions in the order they were added, __and presents the first question whose condition is satisfied, or whose condition is None__. If none of the questions for a key has a condition that is satisfied, all questions for this key are skipped.\n\nA condition can be added to a question by chaining a call to `condition` onto the `one`, `many`, or `raw` call. A list of condition tuples must be passed. The first two values in a tuple, the __key__ and the __value__, are required. The third value, the __operator__, is optional (the default operator is `'=='`).\n\nA __key__ is a key for a previously answered question in the questionnaire. The __key__ is used to look up the answer, and the answer gets compared with the __value__. If their relationship is true under the __operator__, this condition tuple is satisfied.\n\nIf all the condition tuples for a question are satisfied, the question's condition is satisfied.\n\n\n### Condition Operators\nThe default operator is __equals__. The following operators can be passed as strings: `==`, `!=`, `\u003c`, `\u003e`, `\u003c=`, `\u003e=`, and their corresponding operator functions are looked up. If you want to define your own operators, make sure they are functions that accept two values (the values to be compared) and return a boolean.\n\n\n## Questionnaire Options\nThese can be passed to a questionnaire when you instantiate it. You can also change these properties (they have the same names) directly on the questionnaire instance while it's running.\n\n- `show_answers`: show all previous answers above question prompt\n- `can_go_back`: allow users to go back\n\n\n## Writing Your Own Prompters\n__questionnaire__ is easy to extend. Write a prompter function that satisfies the prompter API. When you add a question to your questionnaire, instead of invoking `one`, `many`, or `raw`, invoke the generic `add` function, and pass a function as the `prompter` arg.\n\n__The prompter API is super simple__: a prompter is a function that should display a question and capture user input. It returns this input as an `answer`.\n\n\n### Going Back\nIf you want your prompter to allow users to go back, simply raise a `QuestionnaireGoBack` exception in the body of your prompter function instead of returning the answer. This exception can imported like so: `from questionnaire.prompters import QuestionnaireGoBack`.\n\nWhen you raise this exception in your prompter, you can pass the __number of steps to go back__ into the exception's constructor. For example, `raise QuestionnaireGoBack(2)` will go back two questions. If no value is passed to the constructor, the user goes back one question.\n\n\n## Tests\nFrom the root of the repo, run `python -m unittest discover -v`. Tests in the `tests` directory are run automatically by Travis. These tests cover the `Questionnaire`, `Question` and `Condition` classes. They don't cover prompters, which are completely decoupled from `Questionnaire`.\n\nIf you want to make sure the core prompters are working, the modules in the `examples` directory should be used to test them. Run, for example, `python -m examples.plans` or `python -m examples.activities` from the root of the repo. If anyone wants to help automate these tests that would be great!\n\n\n### Coverage\nTo see coverage stats for the questionnaire package, run `coverage run --source=questionnaire -m unittest discover -v` and `coverage report`.\n\n\n## Contributing\nIf you want to improve __questionnaire__, fork the repo and submit a pull request. Integration tests for the prompters would be nice. I think it would also be nice to refactor the raw prompter to use curses. A boolean __(Y/n)__ prompter might be nice, even though this use case is handled fine by the `one` prompter.\n\n\n## Gotchas\n__questionnaire__ merges `stdout` with `stderr` while the prompters are running. If you run a questionnaire and redirect `stderr` you'll find it contains everything printed to the terminal by `curses`.\n\n\n## License\nThis code is licensed under the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkylebebak%2Fquestionnaire","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkylebebak%2Fquestionnaire","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkylebebak%2Fquestionnaire/lists"}