0%

Missing CSE Homework

Missing CSE是MIT开设的一门的小学期课程,讲述了许多目前CS高等教育中严重缺失但却在工业界广泛应用或能极大提升生产力的工具与知识。

本文记录了部分Homework的解答。

Course overview + the shell

创建一个名为 missing 的文件夹,并在其中创建一个名为 semester 的文件并写入:

1
2
#!/bin/sh
curl --head --silent https://missing.csail.mit.edu

使用 chmodsemester 添加执行权限。

写一段命令将 semester 文件输出的最后更改日期信息到 last-modified.txt 中。

1
2
# Answer
./semester | grep 'last-modified' > 'last-modified.txt'

Shell Tools and Scripting

  • 编写两个bash函数 marcopolo 执行下面的操作。 每当你执行 marco 时,当前的工作目录应当以某种形式保存,当执行 polo 时,无论现在处在什么目录下,都应当 cd 回到当时执行 marco 的目录。 为了方便debug,你可以把代码写在单独的文件 marco.sh 中,并通过 source marco.sh 命令,(重新)加载函数。
    解答:
    macro.sh

    1
    2
    3
    4
    5
    6
    7
    8
    #!/usr/bin/env bash
    macro () {
    export MACRO_DIR=$(pwd)
    }

    polo () {
    cd $MACRO_DIR
    }

    运行过程

  • 假设您有一个命令,它很少出错。因此为了在出错时能够对其进行调试,需要花费大量的时间重现错误并捕获输出。 编写一段bash脚本,运行如下的脚本直到它出错,将它的标准输出和标准错误流记录到文件,并在最后输出所有内容。 加分项:报告脚本在失败前共运行了多少次。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #!/usr/bin/env bash

    n=$(( RANDOM % 100 ))

    if [[ n -eq 42 ]]; then
    echo "Something went wrong"
    >&2 echo "The error was using magic numbers"
    exit 1
    fi

    echo "Everything went according to plan"

    解答:
    将上述内容写入 script.sh,创建 test.sh ,写入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #!/usr/bin/env bash

    status=0

    while [[ $status -eq 0 ]]; do
    ./script.sh >> out 2>> err
    status=$?
    done

    echo "$(wc -l out) before error."

    运行过程

  • 编写一个命令,它可以递归地查找文件夹中所有的HTML文件,并将它们压缩成zip文件。注意,即使文件名中包含空格,您的命令也应该能够正确执行(提示:查看 xargs 的参数 -d
    解答:
    懒得找有HTML的文件夹,就用 .sh 文件代替了。创建文件 zipsh ,写入

    1
    2
    3
    #!/usr/bin/env bash

    find $1 -name '*.sh' -print0 | xargs -0 zip -r sh.zip

    运行过程

Editors (Vim)

这节讲了Vim的一些基本操作,学习一下可以极大提升命令行环境下的工作效率。各种快捷键就不在此赘述(vimtutor是个好东西)。

这里记录一下一个Demo的修改过程。如下是一个有问题的fizz buzz实现:

1
2
3
4
5
6
7
8
9
10
11
12
# fizz.py
def fizz_buzz(limit):
for i in range(limit):
if i % 3 == 0:
print('fizz')
if i % 5 == 0:
print('fizz')
if i % 3 and i % 5:
print(i)

def main():
fizz_buzz(10)
  1. main 方法没有调用
    vim fizz.py
    输入 G 到底文件尾
    输入 o 向后新增一行
    输入 main 方法调用代码
    1
    2
    if __name__ == '__main__':
    main()
  2. fizz_buzz 方法中的循环从0开始
    输入 3G 跳到第3行
    输入 % 跳到 (limit)
    输入 b 向前跳1个词
    输入 i ,将代码变为
    1
    for i in range(1, limit+1):
  3. 5的倍数打印的是”fizz”
    输入 7G 跳到第7行
    输入 ci' 清除”fizz”
    输入”buzz”
  4. 15的倍数打印的”fizz”和”buzz”在两行
    输入 5G 跳到第5行
    输入 $ 跳到行尾
    输入 i ,将代码变为
    1
    print('fizz', end='')
    输入 7G 跳到第7行
    输入 $ 跳到行尾
    输入 . 重复第五行的编辑
  5. main 方法中的10是硬编码
    输入 2G 跳到第2行
    输入 O 在前新增一行,添加
    1
    2
    import sys

    输入 14G 跳到第14行
    输入 % 跳到 (10)
    输入 ci( 清除并编辑括号内内容,改为
    1
    fizz_buzz(int(sys.argv[1]))
    修改后的文件内容为
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # fizz.py
    import sys

    def fizz_buzz(limit):
    for i in range(1, limit+1):
    if i % 3 == 0:
    print('fizz', end='')
    if i % 5 == 0:
    print('buzz', end='')
    if i % 3 and i % 5:
    print(i)

    def main():
    fizz_buzz(int(sys.argv[1]))

    if __name__ == '__main__':
    main()

Data Wrangling

这节介绍了“Data Wrangling”。所谓“Data Wrangling”,描述的是通过一系列操作从原始数据中过滤得到我们需要的数据。在类Unix系统上,这通常可以通过一系列命令和管道来完成。

  • 完成 short interactive regex tutorial
    这是一个循序渐进的Regex教程,这里记录一下答案

    1. 匹配三个字符串 abcdefg / abcde / abc
      abc[d-g]*
    2. 匹配 abc123xyz / define "123" / var g = 123; 中的所有数字
      \d+,或者暴力的 123
    3. 匹配 cat. / 896. / ?=+. 但不匹配 abc1
      .{3}\.,或 ...\.
    4. 匹配 can / man / fan 但不匹配 dan / ran / pan
      [cmf]an,或者 [^drp]an
    5. 匹配 hog / dog 但不匹配 bog
      [hd]og,或者 [^b]og
    6. 匹配 Ana / Bob / Cpc 但不匹配 aax / bby / ccz
      [A-C][n-p][a-c]
    7. 匹配 wazzzzzup / wazzzup 但不匹配 wazup
      waz{2,}up
    8. 匹配 aaaabcc / aabbbbc / aacc 但不匹配 a
      a+b*c+
    9. 匹配 1 file found? / 2 files found? / 24 files found? 但不匹配 No files found.
      \d+ files? found\?
    10. 匹配 1.⊔⊔⊔abc / 2.⊔(tab)abc / 3.⊔⊔⊔⊔⊔⊔⊔⊔⊔⊔⊔abc 但不匹配 4.abc
      \d\.\s+abc
    11. 匹配 Mission: successful 但不匹配 Last Mission: unsuccessful / Next Mission: successful upon capture of target
      ^Mission: successful$
    12. 捕获 file_record_transcript.pdf / file_07241999.pdf 中的文件名,但不匹配 testfile_fake.pdf.tmp
      ^(file.+)\.pdf$
    13. 捕获 Jan 1987 / May 1969 / Aug 2011 中的整个日期和年份(两个捕获组)
      (\w+ (\d+))
    14. 捕获 1280x720 / 1920x1600 / 1024x768 等分辨率中的宽和高
      (\d+)x(\d+)
    15. 匹配 I love cats / I love dogs 但不匹配 I love logs / I love cogs
      I love (cat|dog)s
    16. 匹配 The quick brown fox jumps over the lazy dog. / There were 614 instances of students getting 90.0% or above. / The FCC had to censor the network for saying &$#*@!.
      偷懒的.*,或^The(re)? (.+( |\.))+$
    17. 匹配 3.14529 / -255.34 / 128 / 1.9e10 / 123,340.00 但不匹配 720p
      ^\-?(\d,?)+\.?\d+(e\d+)?$^\-?\d+(,\d+)*(\.\d+(e\d+)?)?$
    18. 捕获 415-555-1234 / 650-555-2345 / (416)555-3456 / 202 555 4567 / 4035555678 / 1 416 555 9292 的区号,即 415 / 650 / 416 / 202 / 403 / 416
      1?(\s|-)?\(?(\d{3})\)?(\s|-)?\d{3}(\s|-)?\d{4}
    19. 捕获 tom@hogwarts.com / tom.riddle@hogwarts.com / tom.riddle+regexone@hogwarts.com / tom@hogwarts.eu.com / potter@hogwarts.com / harry@hogwarts.com / hermione+regexone@hogwarts.com 中的用户名
      ^(\w+(\.\w+)*)(\+\w+)*@\w+(\.\w+)+$
    20. 捕获 <a>This is a link</a> / <a href='https://regexone.com'>Link</a> / <div class='test_style'>Test</div> / <div>Hello <span>world</span></div> 中的HTML标签
      <(\w+)( \w+=\'.+\')*>.*</(\w+)>
    21. 捕获下列文件中图片文件的文件名和扩展名,且不匹配其他文件名:
      .bash_profile / workspace.doc / img0912.jpg / updated_img0912.png / documentation.html / favicon.gif / img0912.jpg.tmp / access.lock
      ^(\w+)\.((jpg)|(png)|(gif))$
    22. 去掉字符串头尾的空白符,捕获文本内容:⊔(tab)⊔(tab)⊔(tab)The quick brown fox... / ⊔⊔jumps over the lazy dog.
      ^\s*(.*)\s*$
    23. 捕获下列日志中的文件名、方法名、行号:
      E/( 1553): at widget.List.makeView(ListView.java:1727)
      E/( 1553): at widget.List.fillDown(ListView.java:652)
      E/( 1553): at widget.List.fillFrom(ListView.java:709)
      跳过:
      W/dalvikvm( 1553): threadid=1: uncaught exception
      E/( 1553): FATAL EXCEPTION: main
      E/( 1553): java.lang.StringIndexOutOfBoundsException
      [A-Z]/\( \d+\): at \w+\.\w+\.(\w+)\((\w+\.\w+):(\d+)\)
    24. 捕获下列URL中的协议名、主机、端口:
      ftp://file_server.com:21/top_secret/life_changing_plans.pdf
      https://regexone.com/lesson/introduction#section
      file://localhost:4040/zip_file
      https://s3cur3-server.com:9999/
      market://search/angry%20birds
      ^(\w+)://([\w\-\.]+)(:(\d+))?
  • /usr/share/dict/words 找出包含至少3个a,且不以s结尾的单词:

    1. 出现次数排名前三的最后两位字母的组合是?这些两位字母组合的个数?
      1
      2
      cat /usr/share/dict/words | grep -E '^(.*a.*){3}.*[^s]$' | sed -E 's/.*(..)$/\1/' | sort | uniq -c | sort -r
      | head -n3
      cat /usr/share/dict/words 打印出文件中的所有单词
      grep -E '^(.*a.*){3}.*[^s]$' 匹配出其中包含至少3个a,且不以s结尾的单词
      sed -E 's/.*(..)$/\1/' 使用正则替换,提取出这些单词的最后两位字母
      sort | uniq -c 将字母组合排序,合并重复项且计数
      sort -r | head -n3 以出现次数为key逆序排序,并提取出现次数前三名的组合
    2. 哪些两位字母组合没有出现?
      学习自这里
      1
      2
      3
      comm -13
      <(cat /usr/share/dict/words | grep -E '^(.*a.*){3}.*[^s]$' | sed -E 's/.*(..)$/\1/' | sort -u)
      <(echo {a..z}{a..z} | tr " " "\n")
  • 使用 sed 进行原地替换是一个很有诱惑力的选择,如 sed s/REGEX/SUBSTITUTION/ input.txt > input.txt 。但这条命令是无法实现目标的,为什么?是否仅对 sed 命令有此限制?查询 sed 的文档,找到原地替换的方法。
    学习自这里

    When the shell sees > index.html in the command line it opens the file index.html for writing, wiping off all its previous contents.

    简言之,在 sed 真正执行前,重定向已经将文件内容清空了。因此这一问题不仅仅只针对 sed ,其他命令同样无法通过这一操作进行原地修改。

    sed-i 选项提供了原地替换,上面的语句应写为:
    sed -i '.tmp' s/REGEX/SUBSTITUTION/ input.txt
    这一语句在执行原地替换前会先生成一个后缀为 .tmp 的备份文件。如果使用 -i '' ,则不会生成任何中间文件。

Command-line Environment

Job Control

  1. 我们可以通过 ps aux | grep 来获得我们启动的job的pid,并kill它们。事实上还有更方便
    的做法。后台启动一个 sleep 10000 的job:使用 pgrep 查找pid。使用 pkill 来结束进程,从而使得无需自行填入pid。(提示:使用 -af
    A:
    查找pid:pgrep -af 'sleep 10000'
    kill进程:pkill -af 'sleep 10000'
  2. 写一个 pidwait 函数,接收一个pid,并等到该进程结束,并执行 ls (提示:kill -0 不会向指定进程发送信号,但会根据进程是否存在返回成功(0)或失败(非0))
    A:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #!/bin/zsh
    pidwait () {
    while kill -0 $1 2>/dev/null
    do
    sleep 1
    done
    echo "process $1 has finished."
    ls
    }

Terminal Multiplexer

tmux tutorial

自定义 tmux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ~/.tmux.conf

#remap prefix from 'C-b' to 'C-a'
unbind C-b
set-option -g prefix C-a
bind-key C-a send-prefix

# Enable mouse control (clickable windows, panes, resizable panes)
# set -g mouse-select-window on
# set -g mouse-select-pane on
# set -g mouse-resize-pane on

# Enable mouse mode (tmux 2.1 and above)
set -g mouse on

Aliases

使用 history | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10 可以从history中读取最常使用的10条命令,为它们设置别名

A:

我的Top 10 cmd list如下:

1
2
3
4
5
6
7
8
9
10
 30 l
35 ssh cbs@192.168.1.149
39 git pull
44 hexo g -d
59 npm start
71 cd ~
75 hexo s
82 ls -a
170 cd ..
849 ls

没想到 hexo 的使用频率还挺高,于是做了以下别名:

1
2
3
4
5
6
7
# ~/.zprofile

# setting hexo aliases
alias hs="hexo s"
alias hgd="hexo g -d"

# ...

Remote Machines

查询 ssh-N-f 标志,实现后台端口转发:

A:

首先在VM上启动一个HTTP服务,监听8090端口,利用 -L 进行端口转发,可以写出命令:

1
ssh -L 8090:localhost:8090 VM

该命令监听本地的8090端口,并将其转发到VM的8090端口。然而该命令的问题在于,在实现端口转发的同时还登录了VM。如果我们不需要登录,就可以使用 -N

1
2
3
# ssh manpage

-N Do not execute a remote command. This is useful for just forwarding ports.

加上 -N 后,端口转发命令变成了

1
ssh -N -L 8090:localhost:8090 VM

我们不会再登入VM了,然而,这条命令仍然存在阻塞问题。阻塞的命令导致我们无法在当前终端中继续其他工作。为了让其在后台工作,我们可以引入 -f

1
2
3
4
5
6
# ssh manpage

-f Requests ssh to go to background just before command execution.
This is useful if ssh is going to ask for passwords or passphrases, but the
user wants it in the background. This implies -n. The recommended way to
start X11 programs at a remote site is with something like ssh -f host xterm.

最终,命令变成了

1
ssh -fN -L 8090:localhost:8090 VM

实现了不登入VM,运行在后台的端口转发