Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Show HN: Command Line Challenge (cmdchallenge.com)
205 points by zoidb on Jan 26, 2017 | hide | past | favorite | 90 comments


It would be nice to have to have some sort of hints for beginners. Hint 1: tells you command, Hint 2: tells which options/flags you might need, Hint 3: shows (a) solution


This was a small side project I completed recently, would love to hear feedback. Thanks!


I really like this; reminiscent of the Python Challenge, but for shell.

You should support bash's double-star wildcard, which works recursively. Several of the challenges that currently require

    find -name 'access.log*' -print0 | xargs -0 somecommand
could just use

    somecommand **/access.log*
instead.

You might also encourage people to look up manpages. For instance, someone who doesn't know about "grep -h" may want to look at the manpage for grep. You could make "man grep" link to http://man7.org/linux/man-pages/man1/grep.1.html .

Tab completion doesn't always include all files; for instance, it doesn't include split-me.txt

Have you considered logging all unique successful solutions (filtering out minor whitespace differences) to each challenge? That would let people who have completed each challenge see other potential approaches to the problem, and perhaps learn about new commands.


> You should support bash's double-star wildcard, which works recursively

It depends on what the educational goals are, but alternatively OP could swing on the opposite direction and go with a very vanilla POSIX sh style shell (maybe dash) to discourage bashisms.

I might of course be bit biased because `find` is one of my favorite utils (generally with -exec instead of -print0|xargs -0) and I practically never use newfangled doublestar globbing


Teaching efficient bash usage and teaching portable shell scripting seem related but distinct. This challenge specifically mentions that it uses bash.


Yes sure, I wasn't trying to say that going full bash is a bad thing. I was merely presenting an alternative.


Ah good point, the commands are run in a bash 4 subshell actually but by default the globstar option is off.


globstar is on by default now so this will work.


> I really like this; reminiscent of the Python Challenge, but for shell. > You should support bash's double-star wildcard, which works recursively. Several of the challenges that currently require > find -name 'access.log' -print0 | xargs -0 somecommand > could just use > somecommand /access.log

This challenge was where I stopped, and for this exact reason. Then again, I am a zsh user, so maybe I'm spoiled...


I stopped after 'Print the relative file paths, one path per line for all files that start with "access.log" in the current directory.' It didn't like my BSD find options.


> It didn't like my BSD find options.

Which options, specifically?


You can use

  shopt -s globstar
and

  **/access.log*
works just it would in bash :)


I actually submitted a PR upstream, and the author merged it and deployed it, so cmdchallenge.com now has globstar enabled.


The finale was weird for me, kinda anticlimactic (back to "hello world"); is that how it's expected to behave? No "finished" message or something? Or is my NoScript interfering somehow?

    She is betraying us! Russia alone must save Europe.
    Our gracious sovereign recognizes his high vocation
    and will be true to it. That is the one thing I have
    faith in! Our good and wonderful sovereign has to
    perform the noblest role on earth, and he is so virtuous
    and noble that God will not forsake him. He will fulfill
    his vocation and crush the hydra of revolution, which
    has become more terrible than ever in the person of this
    murderer and villain!
    #     Correct!
    # You have a new challenge!
    # Print "hello world".
    # Hint: There are many ways to print text on
    # the command line, one way is with the 'echo'
    # command.
    # 
    # Try it below and good luck!
    # 
    bash(0)> printf "hello world"
    hello world
    #     Correct!
    bash(0)>


  # Print "hello world".
  # Hint: There are many ways to print text on
  # the command line, one way is with the 'echo'
  # command.
  # 
  # Try it below and good luck!
  # 
  bash(0)> echo '"hello world".'
  "hello world".
  bash(0)>
  
What is this, Android? Sheesh.


Nach dem Spiel ist vor dem Spiel?

https://en.wikipedia.org/wiki/Sepp_Herberger#References_in_p...

(I would also like it if the site somehow indicated that you got to the end or congratulated you on finishing everything.)


I honestly didn't think anyone would complete all of them :) I'll put something cool at the end, thanks for the suggestion.


I was on the 3rd challenge, listing files in a directory, one per line, and it didn't accept "ls -l", but it did accept "ls"


Same. Seems like the wording on some questions could use tweaking. Not bad overall, though.


If anything, i'd argue that it's incorrect. I first tried `ls -l`, then realized "oh, there could be hidden files so `-l` is not correct. Yet, `ls -la` failed too.

To list "all files", you'd need to include hidden right? Well, to be sure, at least.


I took this to mean it wanted only filenames and nothing else. I used:

    ls -1


Yeah it's particular about the output that it expects.


I guess my point is that "ls" shouldn't be an answer. Typically it will print out multiple files on a line. If it's just checking for output and not the actual command someone entered, there should probably be more than one file in the directory to show that it will actually print one file per line.


The mostly correct answer is `ls -1`. Just mostly, because it falls to the classic unix trap (filenames can contain almost anything) which also shows that the challenge is ill defined. What is the expected result if the filename contains newlines?

And this is not just some hypothetical scenario, just today at work I encountered a file with cursor movement terminal escape sequences in the filename.


`ls -1b` should work correctly in your case. Option -b (or --escape) enables C-style escapes. IMHO, it should be on by default.


Because it is executed outside of a terminal (the execution isn't in javascript) you get a single column output. Probably would be better to be more specific in the descriptions, thanks for the suggestion.


Although it often doesn't get printed like that, shells do consider the output of `ls` to be multiple lines. Try doing `ls > foo.txt` and looking at the output file (or more directly, `ls | wc -l`).


That isn't the shell considering the output one per line.

ls will change the way it writes its output if it detects that stdout is a tty versus a pipe or redirect.


It seems to accept any command that produces the expected output, though. That question should specifically say that it only wants the filenames, which would make it clear that `ls -l` doesn't produce the desired result.


Super-cool! Some pieces of feedback:

1) It would be nice to have something special happen when you solve all the puzzles, rather than going back to the first. Some javascript balloons dropping from the ceiling? ;_)

2) Some of the problems, it's hard to tell why an answer doesn't work. A side-by-side diff showing my results compared to the expected results might help?

3) After solving (perhaps just one, or maybe all), I'd love to see other people's solutions. That'd be quite educational.


I really enjoyed this, it was a nice way to practice/learn some command line tricks. In particular I got better acquainted with the find command. Nice work! My only suggestion would be to have an option to show a solution (yours or perhaps even a user-submitted one) if you get stuck on a particular challenge.


    grep -rl 500 .
wasn't accepted. Am I wrong or is it wrong?


Only filenames matching access.log should be returned.

Unfortunately, since shopt globstar is disabled I ended up needing to do this:

    grep 500 $(find . -type f -iname "access.log*")


You could also include "shopt -s globstar; " at the beginning of your line.


grep -r 500 --include access.log


That returns the filenames in the format "./access.log" but it wanted "access.log". It's a bug.

I used echo -e 'access.log\naccess.log.1\nREADME'. :D


Gotcha. Thanks.


Hahahaha I did the exact same thing


You are: grep matches strings, not integers.

    grep -rl '500'
works just fine.


There is no concept of an integer on an argument vector. Quoting in bash just escapes IFS characters (usually whitespace) to control whether to include them in a given argument on the vector, but these arguments (as passed to exec) are always character strings (with or without quoting).


Interestingly enough

grep -rl "500" .

doesn't work because it returns with the "./".


After completing the challenges (only had to lookup references on bash parameter expansion and how to read lines without parsing them), I would say that it might be helpful to have some reference information available whether via clicking to a third party URL. It'd also be _really_ nifty to be able to compare my solutions against others' solutions.


I'm making something related as a side project too, and the major problem I have right now is showing a terminal on the page. As you can see I'm relying on archaic methods... https://term.surge.sh


Have you considered running the commands directly in JavaScript, rather than via AWS? With some work, you could run everything inside emscripten in the browser, and then you don't need to worry about security, sandboxing, or scaling.


In this case you'd probably be better "just" going for http://stackoverflow.com/questions/6030407/how-does-linux-em... ?


Yeah though this was pretty easy to setup and I was looking to replicate an authentic command line experience including the standard unix utilities.


It looks a bit like the cwd is shared between multiple users accessing the site? I suddenly got errors that access.log disappears, after which my pwd seemed to randomly change.


Nothing is shared in fact every time you submit a command you get a new subshell in a new container. If it is reproducible I will track down the problem.


Fun! Needs tab-complete.


I tried "rm -rf". Not sure if it worked.


I got stuck on this one too. Strangely "find . -delete" worked fine. I'm getting Internal Server Error" now, so perhaps a victim of it's own popularity?


This is a really fun idea, and I enjoyed going thru it. I like the fact that it doesn't matter how you get the answer, as long as it's right (seq 100 | tr '\n' ' ').

This would be a really useful product as part of the interview process for technical people. I'd ask candidates to self-identify their expertise level, let them skip questions, and consult google as much as they wanted. You could have different subjects and maybe mix and match - shell, powershell, python, postgres etc.


The problem I'm having with this is that the solutions I would use to solve these aren't acceptable answers. But it is interesting to find other examples that do work. These shells are extremely restrictive.


    bash(0)> pwd
    Internal Server Error


It could be an intermittent problem as this is getting a lot of traffic right now. If it is reproducible please link the challenge or submit a github issue.


I like this a lot, thanks for putting together. Definitely made me think hard about how much of the command line I actually know.


  bash(0)> grep GET !$
  + grep GET '!$'
  grep: !$: No such file or directory
Mmm. Fake bash.


Every command is in a new subshell


That may be true but the message from grep shows that $! wasn't expanded by the execution evironment.

BTW i get

  # Print "hello world".
  # Hint: There are many ways to print text on
  # the command line, one way is with the 'echo'
  # command.
  # 
  # Try it below and good luck!
  # 
  bash(0)> printf "hello world"
  Internal Server Error
  bash(️)>


history expansion is normally only enabled for interactive shells. They could turn it on, but if there isn't any history because this isn't really interactive, it wouldn't do anything useful.


find . -type f -exec grep -q 500 {} \; -print

Prints the same list of files as grep -l 500 * , but in a different order; doesn't pass the challenge.

Also, grep -Il 500 * doesn't include README, for some reason. file README doesn't work :-(


Interestingly, locale can change one answer.

with LC_COLLATE=en_US.UTF-8,

sort faces.txt | uniq -c

5 (◕‿◕)

uniq treats (︺︹︺) as equal to (◕‿◕).

Copied the file to my machine to test.


I, too, ended up copying the file to my machine.

I also ended up with this: https://gist.github.com/daveloyall/63485c5a08882432b138c0917...


I didn't think we were allowed to even potentially change the order, so I used

  awk '{if (!c[$0]) { print }; c[$0] = 1 }'


Oh ya, I shouldn't have said answer, because uniq is not a way to the answer, just something I naturally reached for while trying to figure out an answer.


    awk '!a[$0]++' faces.txt
is a touch shorter ^^


That's the same approach used by the challenge's original creator (in the GitHub link above). It's a clever solution!


This is really cool, good work! As others have mentioned, hints/etc would definitely be helpful for beginners. I want to post this in our work chat for the less Unix oriented folks, but they'd just be lost


It doesn't handle glob expansion.

  bash(0)> awk '{print $1}' access.log*
  + awk '{print $1}' 'access.log*'
  awk: cannot open access.log* (No such file or directory)


It does appear to handle glob expansion. Some of the levels have files within subdirectories, which isn't always completely clear from the challenge text. In the level you're trying, access.log might not be in the current directory.


Love it! My bash is quite rusty it seems, couldn't get past the 6th or 7th one. Will brush up and do the challenge again. Nice work!


fun, although I did cheat a bit on the corrupted text and pulled the expected output from README instead of the input file :)


The answers I came up with are in https://github.com/jarv/cmdchallenge/blob/master/challenges.... if you are curious.


The awk line for duplicate lines without changing ordering is… wow. Awk is such an underused tool (at least for me.) My solution was:

    awk '{printf "%2d %s\n", NR, $0}' < faces.txt | sort -k 2 -u | sort -n | sed 's/^...//'
Ironically, it also involved awk! But I used it to prepend line numbers, which uniq and sort can ignore, then re-sort it according to line numbers. Very much not an ideal solution!


Mine was more procedural - store each line in a hash as you visit it, and only print lines you haven't already visited. awk is awesome ;-)

awk '!h[$0] {h[$0]=1; print}' faces.txt


I also used this approach but the original creator's version posted on GitHub is remarkably more concise (due to a clever use of the ++ operator).


awk '!h[$0]++' faces.txt

That is clever! Awk golf hole-in-one ;-)


I love awk. I used to be afraid of it, but I started using it just to pluck columns, and over a couple years actually started to grok it.

Also, I learned today that cat takes a -n option to prepend line numbers to the output! My solution ended up being

    cat -n faces.txt | sort -k2 -k1n | uniq -f1 | sort -nk1,1 | cut -f2-


That was a tough one. In the end something like this:

< war_and_peace.txt tr -s '!' | sed 's/!\([a-z]\)/\1/g' | sed 's/!\( [a-z]\)/\1/g' | sed 's/!\.!/./g' | sed 's/ !/ /g'

However, I don't think I learnt much from that. :-)


Mine was a bit more procedural, if that's what you were looking for:

  awk '{l++;gsub("!","");t[1]=4;t[4]=2;t[9]=3;o=$1;for(i=2;i<=NF;i++){o=o " " $i (i==t[l]?"!":"")};print o}' war_and_peace.txt


The description for remove_duplicate_lines needs some clarification, because as written it sounds like either "uniq" or "uniq -u". You should explicitly say that you want the first instance of every duplicated line printed, and all other duplicates not printed, even if not adjacent.


Thanks, also feel free to send a pull requests as I would definitely appreciate contributions.


Ah, I thought you knew seq had a delimiter option! It does actually. :-)

    seq -s ' ' 1 100


Just "echo {1..100}" works too.


challenge "just the files" description says "Print all files in the current directory". You mean "print the names of all files ..."

Lots of the challenges have awkward/ambiguous wording like this.

(If you'd like more detailed feedback, email me -- this username @gmail)


  python: command not found
:(


try perl


grep -R --include="access.log*" "500"

Should this not work for challenge 9?


Nevermind, I had it wrong. The line should have read

"grep 500 -rh --include='access.log*'"


Judging by the domain name I was expecting cmd.exe .

Personally, I think that would be a bit more of a challenge than bash.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: