{"rowid": 215, "title": "Teach the CLI to Talk Back", "contents": "The CLI is a daunting tool. It\u2019s quick, powerful, but it\u2019s also incredibly easy to screw things up in \u2013 either with a mistyped command, or a correctly typed command used at the wrong moment. This puts a lot of people off using it, but it doesn\u2019t have to be this way.\nIf you\u2019ve ever interacted with Slack\u2019s Slackbot to set a reminder or ask a question, you\u2019re basically using a command line interface, but it feels more like having a conversation. (My favourite Slack app is Lunch Train which helps with the thankless task of herding colleagues to a particular lunch venue on time.)\nSame goes with voice-operated assistants like Alexa, Siri and Google Home. There are even games, like Lifeline, where you interact with a stranded astronaut via pseudo SMS, and KOMRAD where you chat with a Soviet AI.\nI\u2019m not aiming to build an AI here \u2013 my aspirations are a little more down to earth. What I\u2019d like is to make the CLI a friendlier, more forgiving, and more intuitive tool for new or reluctant users. I want to teach it to talk back.\nInteractive command lines in the wild\nIf you\u2019ve used dev tools in the command line, you\u2019ve probably already used an interactive prompt \u2013 something that asks you questions and responds based on your answers. Here are some examples:\nYeoman\nIf you have Yeoman globally installed, running yo will start a command prompt.\n\nThe prompt asks you what you\u2019d like to do, and gives you options with how to proceed. Seasoned users will run specific commands for these options rather than go through this prompt, but it\u2019s a nice way to start someone off with using the tool.\nnpm\nIf you\u2019re a Node.js developer, you\u2019re probably familiar with typing npm init to initialise a project. This brings up prompts that will populate a package.json manifest file for that project.\n\nThe alternative would be to expect the user to craft their own package.json, which is more error-prone since it\u2019s in JSON format, so something as trivial as an extraneous comma can throw an error.\nSnyk\nSnyk is a dev tool that checks for known vulnerabilities in your dependencies. Running snyk wizard in the CLI brings up a list of all the known vulnerabilities, and gives you options on how to deal with it \u2013 such as patching the issue, applying a fix by upgrading the problematic dependency, or ignoring the issue (you are then prompted for a reason).\n\nThese decisions get mapped to the manifest and a .snyk file, and committed into the repo so that the settings are the same for everyone who uses that project.\nI work at Snyk, and running the wizard is what made me think about building my own personal assistant in the command line to help me with some boring, repetitive tasks.\nWriting your own\nSomething I do a lot is add bookmarks to styleguides.io \u2013 I pull down the entire repo, copy and paste a template YAML file, and edit to contents. Sometimes I get it wrong and break the site. So I\u2019ve been putting together a tool to help me add bookmarks.\nIt\u2019s called bookmarkbot \u2013 it\u2019s a personal assistant squirrel called Mark who will collect and bury your bookmarks for safekeeping.*\n\n*Fortunately, this metaphor also gives me a charming excuse for any situation where bookmarks sometimes get lost \u2013 it\u2019s not my poorly-written code, honest, it\u2019s just being realistic because sometimes squirrels forget where they buried things!\nWhen you run bookmarkbot, it will ask you for some information, and save that information as a Markdown file in YAML format.\nFor this demo, I\u2019m going to use a Node.js package called inquirer, which is a well supported tool for creating command line prompts. I like it because it has a bunch of different question types; from input, which asks for some text back, confirm which expects a yes/no response, or a list which gives you a set of options to choose from. You can even nest questions, Choose Your Own Adventure style.\nPrerequisites\n\nNode.js\nnpm\nRubyGems (Only if you want to go as far as serving a static site for your bookmarks, and you want to use Jekyll for it)\n\nDisclaimer\nBear in mind that this is a really simplified walkthrough. It doesn\u2019t have any error states, and it doesn\u2019t handle the situation where we save a file with the same name. But it gets you in a good place to start building out your tool.\nLet\u2019s go!\nCreate a new folder wherever you keep your projects, and give it an awesome name (I\u2019ve called mine bookmarks and put it in the Sites directory because I\u2019m unimaginative). Now cd to that directory. \ncd Sites/bookmarks\nLet\u2019s use that example I gave earlier, the trusty npm init.\nnpm init\nPop in the information you\u2019d like to provide, or hit ENTER to skip through and save the defaults. Your directory should now have a package.json file in it. Now let\u2019s install some of the dependencies we\u2019ll need.\nnpm install --save inquirer\nnpm install --save slugify\nNext, add the following snippet to your package.json to tell it to run this file when you run npm start.\n\"scripts\": {\n \u2026\n \"start\": \"node index.js\"\n}\nThat index.js file doesn\u2019t exist yet, so let\u2019s create it in the root of our folder, and add the following:\n// Packages we need\nvar fs = require('fs'); // Creates our file (part of Node.js so doesn't need installing)\nvar inquirer = require('inquirer'); // The engine for our questions prompt\nvar slugify = require('slugify'); // Will turn a string into a usable filename\n\n// The questions\nvar questions = [\n {\n type: 'input',\n name: 'name',\n message: 'What is your name?',\n },\n];\n\n// The questions prompt\nfunction askQuestions() {\n\n // Ask questions\n inquirer.prompt(questions).then(answers => {\n\n // Things we'll need to generate the output\n var name = answers.name;\n\n // Finished asking questions, show the output\n console.log('Hello ' + name + '!');\n\n });\n\n}\n\n// Kick off the questions prompt\naskQuestions();\nThis is just some barebones where we\u2019re including the inquirer package we installed earlier. I\u2019ve stored the questions in a variable, and the askQuestions function will prompt the user for their name, and then print \u201cHello \u201d in the console.\nEnough setup, let\u2019s see some magic. Save the file, go back to the command line and run npm start.\n\nExtending what we\u2019ve learnt\nAt the moment, we\u2019re just saving a name to a file, which isn\u2019t really achieving our goal of saving bookmarks. We don\u2019t want our tool to forget our information every time we talk to it \u2013 we need to save it somewhere. So I\u2019m going to add a little function to write the output to a file.\nSaving to a file\nCreate a folder in your project\u2019s directory called _bookmarks. This is where the bookmarks will be saved.\nI\u2019ve replaced my questions array, and instead of asking for a name, I\u2019ve extended out the questions, asking to be provided with a link and title (as a regular input type), a list of tags (using inquirer\u2019s checkbox type), and finally a description, again, using the input type.\nSo this is how my code looks now:\n// Packages we need\nvar fs = require('fs'); // Creates our file\nvar inquirer = require('inquirer'); // The engine for our questions prompt\nvar slugify = require('slugify'); // Will turn a string into a usable filename\n\n// The questions\nvar questions = [\n {\n type: 'input',\n name: 'link',\n message: 'What is the url?',\n },\n {\n type: 'input',\n name: 'title',\n message: 'What is the title?',\n },\n {\n type: 'checkbox',\n name: 'tags',\n message: 'Would you like me to add any tags?',\n choices: [\n { name: 'frontend' },\n { name: 'backend' },\n { name: 'security' },\n { name: 'design' },\n { name: 'process' },\n { name: 'business' },\n ],\n },\n {\n type: 'input',\n name: 'description',\n message: 'How about a description?',\n },\n];\n\n// The questions prompt\nfunction askQuestions() {\n\n // Say hello\n console.log('\ud83d\udc3f Oh, hello! Found something you want me to bookmark?\\n');\n\n // Ask questions\n inquirer.prompt(questions).then((answers) => {\n\n // Things we'll need to generate the output\n var title = answers.title;\n var link = answers.link;\n var tags = answers.tags + '';\n var description = answers.description;\n var output = '---\\n' +\n 'title: \"' + title + '\"\\n' +\n 'link: \"' + link + '\"\\n' +\n 'tags: [' + tags + ']\\n' +\n '---\\n' + description + '\\n';\n\n // Finished asking questions, show the output\n console.log('\\n\ud83d\udc3f All done! Here is what I\\'ve written down:\\n');\n console.log(output);\n\n // Things we'll need to generate the filename\n var slug = slugify(title);\n var filename = '_bookmarks/' + slug + '.md';\n\n // Write the file\n fs.writeFile(filename, output, function () {\n console.log('\\n\ud83d\udc3f Great! I have saved your bookmark to ' + filename);\n });\n\n });\n\n}\n\n// Kick off the questions prompt\naskQuestions();\nThe output is formatted into YAML metadata as a Markdown file, which will allow us to turn it into a static HTML file using a build tool later. Run npm start again and have a look at the file it outputs.\n\nGetting confirmation\nBefore the user makes critical changes, it\u2019s good to verify those changes first. We\u2019re going to add a confirmation step to our tool, before writing the file. More seasoned CLI users may favour speed over a \u201chey, can you wait a sec and just check this is all ok\u201d step, but I always think it\u2019s worth adding one so you can occasionally save someone\u2019s butt.\nSo, underneath our questions array, let\u2019s add a confirmation array.\n// Packages we need\n\u2026\n// The questions\n\u2026\n\n// Confirmation questions\nvar confirm = [\n {\n type: 'confirm',\n name: 'confirm',\n message: 'Does this look good?',\n },\n];\n\n// The questions prompt\n\u2026\n\nAs we\u2019re adding the confirm step before the file gets written, we\u2019ll need to add the following inside the askQuestions function:\n// The questions prompt\nfunction askQuestions() {\n // Say hello\n \u2026\n // Ask questions\n inquirer.prompt(questions).then((answers) => {\n \u2026\n // Things we'll need to generate the output\n \u2026\n // Finished asking questions, show the output\n \u2026\n\n // Confirm output is correct\n inquirer.prompt(confirm).then(answers => {\n\n // Things we'll need to generate the filename\n var slug = slugify(title);\n var filename = '_bookmarks/' + slug + '.md';\n\n if (answers.confirm) {\n // Save output into file\n fs.writeFile(filename, output, function () {\n console.log('\\n\ud83d\udc3f Great! I have saved your bookmark to ' +\n filename);\n });\n } else {\n // Ask the questions again\n console.log('\\n\ud83d\udc3f Oops, let\\'s try again!\\n');\n askQuestions();\n }\n\n });\n\n });\n}\n\n// Kick off the questions prompt\naskQuestions();\nNow run npm start and give it a go!\n\nTyping y will write the file, and n will take you back to the start. Ideally, I\u2019d store the answers already given as defaults so the user doesn\u2019t have to start from scratch, but I want to keep this demo simple.\nServing the files\nNow that your bookmarking tool is successfully saving formatted Markdown files to a folder, the next step is to serve those files in a way that lets you share them online. The easiest way to do this is to use a static-site generator to convert your YAML files into HTML, and pop them all on one page. Now, you\u2019ve got a few options here and I don\u2019t want to force you down any particular path, as there are plenty out there \u2013 it\u2019s just a case of using the one you\u2019re most comfortable with.\nI personally favour Jekyll because of its tight integration with GitHub Pages \u2013 I don\u2019t want to mess around with hosting and deployment, so it\u2019s really handy to have my bookmarks publish themselves on my site as soon as I commit and push them using Git.\nI\u2019ll give you a very brief run-through of how I\u2019m doing this with bookmarkbot, but I recommend you read my Get Started With GitHub Pages (Plus Bonus Jekyll) guide if you\u2019re unfamiliar with them, because I\u2019ll be glossing over some bits that are already covered in there.\nSetting up a build tool\nIf you haven\u2019t already, install Jekyll and Bundler globally through RubyGems. Jekyll is our static-site generator, and Bundler is what we use to install Ruby dependencies.\ngem install jekyll bundler\nIn my project folder, I\u2019m going to run the following which will install the Jekyll files we\u2019ll need to build our listing page. I\u2019m using --force, otherwise it will complain that the directory isn\u2019t empty.\njekyll new . --force\nIf you check your project folder, you\u2019ll see a bunch of new files. Now run the following to start the server:\nbundle exec jekyll serve\nThis will build a new directory called _site. This is where your static HTML files have been generated. Don\u2019t touch anything in this folder because it will get overwritten the next time you build.\nNow that serve is running, go to http://127.0.0.1:4000/ and you\u2019ll see the default Jekyll page and know that things are set up right. Now, instead, we want to see our list of bookmarks that are saved in the _bookmarks directory (make sure you\u2019ve got a few saved). So let\u2019s get that set up next.\nOpen up the _config.yml file that Jekyll added earlier. In here, we\u2019re going to tell it about our bookmarks. Replace everything in your _config.yml file with the following:\ntitle: My Bookmarks\ndescription: These are some of my favourite articles about the web.\nmarkdown: kramdown\nbaseurl: /bookmarks # This needs to be the same name as whatever you call your repo on GitHub.\ncollections:\n - bookmarks\nThis will make Jekyll aware of our _bookmarks folder so that we can call it later. Next, create a new directory and file at _layouts/home.html and paste in the following.\n\n\n\n \n {{site.title}}\n \n\n\n\n\n

{{site.title}}

\n

{{site.description}}

\n\n \n\n\n\n\nRestart Jekyll for your config changes to kick in, and go to the url it provides you (probably http://127.0.0.1:4000/bookmarks, unless you gave something different as your baseurl).\n\nIt\u2019s a decent start \u2013 there\u2019s a lot more we can do in this area but now we\u2019ve got a nice list of all our bookmarks, let\u2019s get it online!\nIf you want to use GitHub Pages to host your files, your first step is to push your project to GitHub. Go to your repository and click \u201csettings\u201d. Scroll down to the section labelled \u201cGitHub Pages\u201d, and from here you can enable it. Select your master branch, and it will provide you with a url to view your published pages.\n\nWhat next?\nNow that you\u2019ve got a framework in place for publishing bookmarks, you can really go to town on your listing page and make it your own. First thing you\u2019ll probably want to do is add some CSS, then when you\u2019ve added a bunch of bookmarks, you\u2019ll probably want to have some filtering in place for the tags, perhaps extend the types of questions that you ask to include an image (if you\u2019re feeling extra-fancy, you could just ask for a url and pull in metadata from the site itself). Maybe you\u2019ve got an idea that doesn\u2019t involve bookmarks at all.\nYou could use what you\u2019ve learnt to build a place where you can share quotes, a list of your favourite restaurants, or even Christmas gift ideas.\nHere\u2019s one I made earlier\n\nMy demo, bookmarkbot, is on GitHub, and I\u2019ve reused a lot of the code from styleguides.io. Feel free to grab bits of code from there, and do share what you end up making!", "year": "2017", "author": "Anna Debenham", "author_slug": "annadebenham", "published": "2017-12-11T00:00:00+00:00", "url": "https://24ways.org/2017/teach-the-cli-to-talk-back/", "topic": "code"}