為什麼優秀的程式設計師喜歡命令列?

邱俊濤發表於2017-03-28

優秀的程式設計師

要給優秀的程式設計師下一個明確的定義無疑是一件非常困難的事情。擅長抽象思維、動手能力強、追求效率、喜歡自動化、願意持續學習、對程式碼質量有很高的追求等等,這些維度都有其合理性,不過又都略顯抽象和主觀。

我對於一個程式設計師是否優秀,也有自己的標準,那就是TA對命令列的熟悉/喜愛程度。這個特點可以很好的看出TA是否是一個優秀的(或者潛在優秀的)程式設計師。我周圍就有很多非常牛的程式設計師,無一例外都非常擅長在命令列中工作。那什麼叫熟悉命令列呢?簡單來說,就是90%的日常工作內容可以在命令列完成。

當然,喜歡/習慣使用命令列可能只是表象,其背後包含的實質才是優秀的程式設計師之所以優秀的原因。

自動化

Perl語言的發明者Larry Wall有一句名言:

The three chief virtues of a programmer are: Laziness, Impatience and Hubris. – Larry Wall

懶惰(Laziness)這個特點位於程式設計師的三大美德之首:唯有懶惰才會驅動程式設計師儘可能的將日常工作自動化起來,解放自己的雙手,節省自己的時間。相比較而言,不得不說,GUI應用天然就是為了讓自動化變得困難的一種設計(此處並非貶義,GUI有著自己完全不同的目標群體)。

GUI更強調的是與人類的直接互動:通過視覺手段將資訊以多層次的方式呈現,使用視覺元素進行指引,最後系統在後臺進行實際的處理,並將最終結果以視覺手段展現出來。

這種更強調互動過程的設計初衷使得自動化變得非常困難。另一方面,由於GUI是為互動而設計的,它的響應就不能太快,至少要留給操作者反應時間(甚至有些使用者操作需要人為的加入一些延遲,以提升使用者體驗)。

程式設計師的日常工作

程式設計師除了寫程式碼之外,還有很多事情要做,比如自動化測試、基礎設施的配置和管理、持續整合/持續釋出環境,甚至有些團隊還需要做一些與運維相關的事情(線上問題監控,環境監控等)。

  • 開發/測試
  • 基礎設施管理
  • 持續整合/持續釋出
  • 運維(監控)工作
  • 娛樂

而這一系列的工作背後,都隱含了一個自動化的需求。在做上述工作時,優秀的程式設計師會努力將其自動化,如果有工具就使用工具;如果沒有,就開發一個新的工具。這種努力讓一切都儘可能自動化起來的哲學起源於UNIX世界。

而UNIX哲學的實際體現則是通過命令列來完成的。

Where there is a shell, there is a way.

UNIX程式設計哲學

關於UNIX哲學,其實坊間有多個版本,這裡有一個比較詳細的清單。雖然有不同的版本,但是有很多一致的地方:

  1. 小即是美
  2. 讓程式只做好一件事
  3. 儘可能早地建立原型(然後逐步演進)
  4. 資料應該儲存為文字檔案
  5. 避免使用可定製性低下的使用者介面

審視這些條目,我們會發現它們事實上促成了自動化一切的可能性。這裡列舉一些小的例子,我們來看看命令列工具是如何通過應用這些哲學來簡化工作、提高效率的。一旦你熟練掌握這些技能,就再也無法離開它,也再也忍受不了低效而複雜的各種GUI工具了。

命令列如何提升效率

一個高階計算器

在我的程式設計生涯早期,讀過的最為振奮的一本書是《UNIX程式設計環境》,和其他基本UNIX世界的大部頭比起來,這本書其實還是比較小眾的。我讀大二的時候這本書已經出版了差不多22年(中文版也已經有7年了),有一些內容已經過時了,比如沒有返回值的main函式、外接的引數列表等等,不過在學習到HOC(High Order Calculator)的全部開發過程時,我依然被深深的震撼到了。

簡而言之,這個HOC語言的開發過程需要這樣幾個元件:

  • 詞法分析器lex
  • 語法分析器yacc
  • 標準數學庫stdlib

另外還有一些自定義的函式等,最後通過make連線在一起。我跟著書上的講解,對著書把所有程式碼都敲了一遍。所有的操作都是在一臺很老的IBM的ThinkPad T20上完成的,而且全部都在命令列中進行(當然,還在命令列裡聽著歌)。

這也是我第一次徹底被UNIX的哲學所折服的體驗:

  • 每個工具只做且做好一件事
  • 工具可以協作起來
  • 一切面向文字

下面是書中的Makefile指令碼,通過簡單的配置,就將一些各司其職的小工具協作起來,完成一個程式語言程式的預編譯、編譯、連結、二進位制生成的動作。

YFLAGS = -d
OBJS = hoc.o code.o init.o math.o symbol.o

hoc5:    $(OBJS)
    cc $(OBJS) -lm -o hoc5

hoc.o code.o init.o symbol.o: hoc.h

code.o init.o symbol.o: x.tab.h

x.tab.h: y.tab.h
    -cmp -s x.tab.h y.tab.h || cp y.tab.h x.tab.h

pr:    hoc.y hoc.h code.c init.c math.c symbol.c
    @pr $?
    @touch pr

clean:
    rm -f $(OBJS) [xy].tab.[ch]

雖然現在來看,這本書的很多內容已經過期(特別是離它第一次出版已經過去了近30年),有興趣的朋友可以讀一讀。這裡有一個Lex/Yacc的小例子的小例子,有興趣的朋友可以看看。

當然,如果你使用現在最先進的IDE(典型的GUI工具),其背後做的事情也是同樣的原理:生成一個Makefile,然後在幕後呼叫它。

基礎設施自動化

開發過程中,工程師還需要關注的一個問題是:軟體執行的環境。我在學生時代剛開始學習Linux的時候,會在Windows機器上裝一個虛擬機器軟體VMWare,然後在VMWare中安裝一個Redhat Linux 9

這樣當我不小心把Linux玩壞了之後,只需要重灌一下就行了,不影響我的其他資料(比如課程作業、文件之類)。不過每次重灌也挺麻煩,需要找到iso映象檔案,再掛載到本地的虛擬光碟機上,然後再用VMWare來安裝。

而且這些動作都是在GUI裡完成的,每次都要做很多重複的事情:找映象檔案,使用虛擬光碟機軟體掛載,啟動VMWare,安裝Linux,配置個人偏好,配置使用者名稱/密碼等等。熟練之後,我可以在30 - 60分鐘內安裝和配置好一個新的環境。

Vagrant

後來我就發現了Vagrant,它支援開發者通過配置的方式將機器描述出來,然後通過命令列的方式來安裝並啟動,比如下面這個配置:

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise64"
  config.vm.network "private_network", :ip => "192.168.2.100"
end

它定義了一個虛擬機器,使用Ubuntu Precise 64的映象,然後為其配置一個網路地址192.168.2.100,定義好之後,我只需要執行:

$ vagrant up

我的機器就可以在幾分鐘內裝好,因為這個動作是命令列裡完成的,我可以在持續整合環境裡做同樣的事情 – 只需要一條命令。定義好的這個檔案可以在團隊內共享,可以放入版本管理,團隊裡的任何一個成員都可以在幾分鐘內得到一個和我一樣的環境。

Ansible

一般,對於一個軟體專案而言,一個全新的作業系統基本上沒有任何用處。為了讓應用跑起來,我們還需要很多東西。比如Web伺服器、Java環境、cgi路徑等,除了安裝一些軟體之外,還有大量的配置工作要做,比如apache httpd伺服器的文件根路徑,JAVA_HOME環境變數等等。

這些工作做好了,一個環境才算就緒。我記得在上一個專案上,不小心把測試環境的Tomcat目錄給刪除了,結果害的另外一位同事花了三四個小時才把環境恢復回來(包括重新安裝Tomcat,配置一些JAVA_OPTS,應用的部署等)。

不過好在我們有很多工具可以幫助開發者完成環境的自動化準備,比如:Chef、 PuppetAnsible。只需要一些簡單的配置,然後結合一個命令列應用,整個過程就可以自動化起來了:

- name: setup custom repo
  apt: pkg=python-pycurl state=present

- name: enable carbon
  copy: dest=/etc/default/graphite-carbon content='CARBON_CACHE_ENABLED=true'

- name: install graphite and deps
  apt: name={{ item }} state=present
  with_items: packages

- name: install graphite and deps
  pip: name={{ item }} state=present
  with_items: python_packages

- name: setup apache
  copy: src=apache2-graphite.conf dest=/etc/apache2/sites-available/default
  notify: restart apache

- name: configure wsgi
  file: path=/etc/apache2/wsgi state=directory

上邊的配置描述了安裝graphite-carbon、配置apahce等很多手工的勞動,開發者現在只需要執行:

$ ansible

就可以將整個過程自動化起來。現在如果我不小心把Tomcat刪了,只需要幾分鐘就可以重新配置一個全新的,當然整個過程還是自動的。這在GUI下完全無法想象,特別是在有如此多的定製內容的場景下。

持續整合/持續釋出

日常開發任務中,除了實際的編碼和環境配置之外,另一大部分內容就是持續整合/持續釋出了。藉助於命令列,這個動作也可以非常高效和自動化。

Jenkins

持續整合/持續釋出已經是很多企業IT的基本配置了。各個團隊通過持續整合環境來編譯程式碼、靜態檢查、執行單元測試、端到端測試、生成報告、打包、部署到測試環境等等。

比如在Jenkins環境中,在最前的版本中,要配置一個構建任務需要很多的GUI操作,不過在新版本中,大部分操作都已經可以寫成指令碼。

這樣的方式,使得自動化變成了可能,要複製一個已有的pipline,或者要修改一些配置、命令、變數等等,再也不需要用滑鼠點來點去了。而且這些程式碼可以納入專案程式碼庫中,和其他程式碼一起被管理、維護,變更歷史也更容易追蹤和回滾(在GUI上,特別是基於Web的,回滾操作基本上屬於不可能)。

node {
   def mvnHome

   stage('Preparation') { // for display purposes
      git 'https://github.com/jglick/simple-maven-project-with-tests.git'
      mvnHome = tool 'M3'
   }

   stage('Build') {
      sh "'${mvnHome}/bin/mvn' -Dmaven.test.failure.ignore clean package"
   }

   stage('Results') {
      junit '*/target/surefire-reports/TEST-.xml'
      archive 'target/*.jar'
   }
}

上面這段groovy指令碼定義了三個階段,每個階段中分別有自己的命令,這種以程式碼來控制的方式顯然比GUI編輯的方式更加高效,自動化也變成了可能。

運維工作

自動化監控

Graphite是一個功能強大的監控工具,不過其背後的理念倒是很簡單:

  • 儲存基於時間線的資料
  • 將資料渲染成圖,並定期重新整理

使用者只需要將資料按照一定格式定期傳送給Graphite,剩下的事情就交給Graphite了,比如它可以消費這樣的資料:

instance.prod.cpu.load 40 1484638635
instance.prod.cpu.load 35 1484638754
instance.prod.cpu.load 23 1484638812

第一個欄位表示資料的名稱,比如此處instance.prod.cpu.load表示prod例項的CPU負載,第二個欄位表示資料的,最後一個欄位表示時間戳。

這樣,Graphite就會將所有同一名稱下的值按照時間順序畫成圖。

預設地,Graphite會監聽一個網路埠,使用者通過網路將資訊傳送給這個埠,然後Graphite會將資訊持久化起來,然後定期重新整理。簡而言之,只需要一條命令就可以做到傳送資料:

echo "instance.prod.cpu.load 23 </span>date +%s<span class="pl-pds">" | nc -q0 graphite.server 2003

date +%s會生成當前時間戳,然後通過echo命令將其拼成一個完整的字串,比如:

instance.prod.cpu.load 23 1484638812

然後通過管道|將這個字串通過網路傳送給graphite.server這臺機器的2003埠。這樣資料就被記錄在graphite.server上了。

定時任務

如果我們要自動的將資料每隔幾秒就傳送給graphite.server,只需要改造一下這行命令:

  1. 獲取當前CPU的load
  2. 獲取當前時間戳
  3. 拼成一個字串
  4. 傳送給graphite.server2003
  5. 每隔5分鐘做重複一下1-4

獲取CPU的load在大多數系統中都很容易:

ps -A -o %cpu

這裡的引數:

  • -A表示統計所有當前程式
  • -o %cpu表示僅顯示%cpu列的數值

這樣可以得到每個程式佔用CPU負載的數字:

%CPU
  12.0
  8.2
  1.2
  ...

下一步是將這些數字加起來。通過awk命令,可以很容易做到這一點:

$ awk '{s+=$1} END {print s}'

比如要計算1 2 3的和:

$ echo "1\n2\n3" | awk '{s+=$1} END {print s}'
6

通過管道可以講兩者連起來:

$ ps -A -o %cpu | awk '{s+=$1} END {print s}'

我們測試一下效果:

$ ps -A -o %cpu | awk '{s+=$1} END {print s}'
28.6

看來還不錯,有個這個指令碼,通過crontab來定期呼叫即可:

#!/bin/bash
SERVER=graphite.server
PORT=2003
LOAD=</span>ps -A -o %cpu <span class="pl-k">|</span> awk <span class="pl-s"><span class="pl-pds">'</span>{s+=$1} END {print s}<span class="pl-pds">'</span></span><span class="pl-pds">

echo "instance.prod.cpu.load ${LOAD} </span>date +%s<span class="pl-pds">" | nc -q0 ${SERVER} ${PORT}

當然,如果使用Grafana等強調UI的工具,可以很容易的做的更加酷炫:

想想用GUI應用如何做到這些工作。

娛樂

命令列的MP3播放器

最早的時候,有一個叫做mpg123的命令列工具,用來播放MP3檔案。不過這個工具是商用的,於是就有人寫了一個工具,叫mpg321,基本上是mpg123的開源克隆。不過後來mpg123自己也開源了,這是後話不提

將我的所有mp3檔案的路徑儲存成一個檔案,相當於我的歌單:

$ ls /Users/jtqiu/Music/*.mp3 > favorites.list
$ cat favorites.list
...
/Users/jtqiu/Music/Rolling In The Deep-Adele.mp3
/Users/jtqiu/Music/Wavin' Flag-K'Naan.mp3
/Users/jtqiu/Music/藍蓮花-許巍.mp3
...

然後我將這個歌單交給mpg321去在後臺播放:

$ mpg321 -q --list favorites.list &
[1] 10268

這樣我就可以一邊寫程式碼一邊聽音樂,如果聽煩了,只需要將這個後臺任務切換到前臺fg,然後就可以關掉了:

$ fg
[1]  + 10268 running    mpg321 -q --list favorites.list

小結

綜上,優秀的程式設計師藉助命令列的特性,可以成倍(有時候是跨越數量級的)提高工作效率,從而有更多的時間進行思考、學習新的技能,或者開發新的工具幫助某項工作的自動化。這也是優秀的程式設計師之所以優秀的原因。而面向手工的、原始的圖形介面會拖慢這個過程,很多原本可以自動化起來的工作被淹沒在“簡單的GUI”之中。

最後補充一點,本文的關鍵在於強調優秀的程式設計師與命令列的關係,而不在GUI程式和命令列的優劣對比。GUI程式當然有其使用場景,比如做3D建模、GIS系統、設計師的創作、圖文並茂的字處理軟體、電影播放器、網頁瀏覽器等等。

應該說,命令列優秀的程式設計師之間更多是關聯關係,而不是因果關係。在程式設計師日常的工作中,涉及到的更多的是一些需要命令列工具來做支援的場景。如果走極端,在不適合的場景中強行使用命令列,而置效率於不顧,則未免有點矯枉過正,南轅北轍了。

相關文章