2013年12月6日 星期五

Bash 內建的 getopts 與外部的 getopt 的使用方式

通常在 GNU/Linux 系統底下使用 command 時都會有帶一些額外的參數,像是以下的例子:

$ echo `seq 0 9` | xargs -n1 | wc -l

利用 `seq 0 9` 產生出 0 到 9 的數字,然後 echo 輸出,再用 xargs -n1 利用空白分開每個數字插入換行符號,然後再導到 wc -l 去計算行數。(其實寫成 `seq 0 9 | wc -l` 也有同樣的效果,利用 hexdump -C 就可以看出其中的不同)

其中的 wc -l 也可以寫成 wc --lines 這樣更清楚、更容易理解。

如果說我們想要利用 Bash shell 寫一個同樣的功能,就可以寫成下面這樣:

#!/bin/bash

while getopts l name; do
    case "$name" in
        ('l')
            line=1
            ;;
    esac
    shift
done

if [ -n "$line" ]; then
    awk 'END { print NR }' "$@"
fi

跟原本的 wc -l 提功同樣功能只是換成使用 awk 這個指令來計算行數。

然而 Bash 內建的 getopts 無法直接提供 --lines 這樣的使用方式,於是就可以改用外部的 getopt 指令來達成。

#!/bin/bash

set -e
eval set -- $(getopt -o l -l "lines" -- "$@")
set +e

while :; do
    case "$1" in
        ('-l'|'--lines')
            line=1
            ;;
        ('--')
            shift
            break
            ;;
    esac
    shift
done

if [ -n "$line" ]; then
    awk 'END { print NR }' "$@"
fi

如果說要利用 Bash 內建的 getopts 寫一個 xargs -n1 這樣的功能,則可以寫成以下這樣:

#!/bin/bash

while getopts n: name; do
    case "$name" in
        ('n')
            max="$OPTARG"
            ;;
    esac
    [ "$OPTIND" = 2 ] && shift
    [ "$OPTIND" = 3 ] && shift 2
done

if [ -n "$max" ]; then
    awk "{for (i = 1 ; i <= NF ; i++) { if (i % $max == 0) printf \"%s\\n\", i; else printf \"%s \", i } }" "$@"
fi

如果要支援 xargs --max-lines=1 這樣的使用方式,則可以利用外部的 getopt 改寫成以下這樣:

#!/bin/bash
# filename: xargs.sh

set -e
eval set -- $(getopt -o "n:" -l "max-lines:" -- "$@")
set +e

while :; do
    case "$1" in
        ('-n'|'--max-lines')
            max="$2"
            shift 2
            ;;
        ('--')
            shift
            break
            ;;
    esac
done

if [ -n "$max" ]; then
    awk "{for (i = 1 ; i <= NF ; i++) { if (i % $max == 0) printf \"%s\\n\", i; else printf \"%s \", i } }" "$@"
fi

這樣就可以使用 `xargs.sh --max-lines=1` 了。

2013年12月4日 星期三

Debian/Ubuntu 無人值守的系統更新

Debian/Ubuntu 上面有一個套件叫做 unattended-upgrades - automatic installation of security upgrades 可以用來設定系統的自動更新,這對於一些特別講究系統安全的伺服器特別有用。

以 Ubuntu 12.04 為例,安裝完成及在下面 APT::Periodic::Unattended-Upgrade 打開之後,預設上只會做安全更新,但是我想要它做一般性的系統更新,於是就修改 /etc/apt/apt.conf.d/50unattended-upgrades

// Automatically upgrade packages from these (origin:archive) pairs
Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}-security";
        "${distro_id}:${distro_codename}-updates";
//      "${distro_id}:${distro_codename}-proposed";
//      "${distro_id}:${distro_codename}-backports";
};

// Do automatic removal of new unused dependencies after the upgrade
// (equivalent to apt-get autoremove)
Unattended-Upgrade::Remove-Unused-Dependencies "true";

// Automatically reboot *WITHOUT CONFIRMATION* if a
// the file /var/run/reboot-required is found after the upgrade
Unattended-Upgrade::Automatic-Reboot "true";

簡單說就是包含使用 precise-updates 上面的更新,更新之後再用 apt-get autoremove 把多餘的套件移除掉,如果有些更新需要重開機才會生效的話,就重開機吧。

然後再修改一下 /etc/apt/apt.conf.d/10periodic

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";

意思是每日檢查更新,如果有更新的話先下載,每週自動清除不需要的套件快取,使用無人值守自動更新。

關於 linux kernel 的更新,有一個腳本程式 /etc/kernel/postinst.d/apt-auto-removal 產生 /etc/apt/apt.conf.d/01autoremove-kernels,用來保留當下正在使用、或是最新安裝、或是上一份使用的 linux kernel。

至於詳細的運作細節則可以參考 /etc/cron.daily/apt 裡面的內容。

2013年12月2日 星期一

VirtualBox Guest 裡面共用資料夾的自動掛載

VirtualBox 有提供一個方便的功能,能將 Host 的某個資料夾自動掛載到 Guest 裡面使用,只是自動掛載的權限不是預設的使用者。

sylee@vbox:~$ mount
/dev/sda1 on / type ext4 (rw,errors=remount-ro)
proc on /proc type proc (rw,noexec,nosuid,nodev)
sysfs on /sys type sysfs (rw,noexec,nosuid,nodev)
none on /sys/fs/fuse/connections type fusectl (rw)
none on /sys/kernel/debug type debugfs (rw)
none on /sys/kernel/security type securityfs (rw)
udev on /dev type devtmpfs (rw,mode=0755)
devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=0620)
tmpfs on /run type tmpfs (rw,noexec,nosuid,size=10%,mode=0755)
none on /run/lock type tmpfs (rw,noexec,nosuid,nodev,size=5242880)
none on /run/shm type tmpfs (rw,nosuid,nodev)
vbox on /media/sf_vbox type vboxsf (gid=999,rw)
sylee@vbox:~$ id
uid=1000(sylee) gid=1000(sylee) groups=1000(sylee),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),111(lpadmin),112(sambashare)

這時候只要參考以前寫過的『如何解決在 Ubuntu 上面安裝 VirtualBox 後缺少 vboxusers 群組權限的問題』,將預設的使用者加入到 vboxsf 這個群組裡面,再重新登入後,就可以自由地使用 /media/sf_vbox 底下的檔案了。

sylee@vbox:~$ sudo usermod -a -G vboxsf sylee

隨意拿了 linux-generic-lts-quantal_3.5.0.44.50_amd64.deb linux-signed-generic-lts-quantal_3.5.0.44.50_amd64.deb linux-signed-image-generic-lts-quantal_3.5.0.44.50_amd64.deb linux-image-3.5.0-44-generic_3.5.0-44.67~precise1_amd64.deb linux-signed-image-3.5.0-44-generic_3.5.0-44.67~precise1_amd64.deb 來測試。

使用 vboxsf 來複製檔案。

real    0m0.661s
user    0m0.004s
sys     0m0.284s

使用 sshfs 來複製檔案。

real    0m2.276s
user    0m0.000s
sys     0m0.103s

sshfs 掰~

2013年9月25日 星期三

Debian/Ubuntu 上好用的 Spotify 網路音樂串流軟體

有高興看到有原生支援 Debian/Ubuntu 的網路音樂軟體。

在 Ubuntu 上面使用的方法如下:

$ echo "deb http://repository.spotify.com stable non-free" | sudo tee /etc/apt/sources.list.d/spotify.list
$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 94558F59
$ sudo apt-get update
$ sudo apt-get install spotify-client

然後就可以執行 Spotify 來使用了。

目前的 Spotify 似乎無法自動偵測到正體中文的環境,所以要手動去偏好設定裡面調整。

最後一點小小的想法,看到許多好用的軟體都會在推出的時候,都會同時支援跨平台的 Windows/Mac/Linux 桌面系統,還有 Android 與 iOS 的移動平台,也許 Linux 桌面的使用族群相對稀少,也許支援 Linux 並沒有什麼高獲利可言,但是卻會讓人感受到該軟體對於使用者的誠意在哪裡,套一句從 Even Wu 那邊看到的:「正確 > 進度,是我的理念。因為正確會有未來的尾速!」。

2013年9月18日 星期三

使用 Ubuntu Juju 中的 Local Provider (Linux Container) 心得

Ubuntu Juju 是一套 Canonical 設計給使用 Ubuntu 做為雲端平台的一套工具。

基本概念是輕鬆地使用一堆專家們精心寫好的 Juju Charms,無痛地將網路服務部署到使用 Ubuntu 的雲端平台上面。

Ubuntu Juju 目前支援的雲端平台有 Amazon Web Services (AWS), Microsoft Azure, HP Cloud, OpenStack, MAAS, Local,而 Ubuntu Juju 的工具本身可以在 Ubuntu, Mac OS X, Windows 上面安裝使用。

而 Ubuntu Juju 的 Local Provider 就是利用 Linux Container (LXC) 來在自己的主機上面架設使用 Ubuntu Juju,以下就是介紹 Local Provider 的設定方法。(窮人省錢的雲端解決方案)

首先當然要有一個 Ubuntu 12.04 以上的系統,然後先要加入 ppa:juju/stable 這個 PPA 然後更新索引:

$ sudo apt-add-repository ppa:juju/stable
$ sudo apt-get update

接下來安裝所需要的軟體套件。

如果是 Ubuntu 12.04 的話執行:

$ sudo apt-get install juju-local linux-image-generic-lts-raring linux-headers-generic-lts-raring

然後重新啟動系統來使用 linux-lts-raring 的 kernel

如果是其它比 Ubuntu 12.04 新的版本的話執行:

$ sudo apt-get install juju-local

然後檢查一下 MongoDB 有沒有支援 SSL (這點很重要,一定要支援 SSL 否則就無法使用):

$ mongod -h | grep ssl
  --sslOnNormalPorts      use ssl on configured ports
  --sslPEMKeyFile arg     PEM file for ssl
  --sslPEMKeyPassword arg PEM file password

還有要檢查一下 LXC 的 DHCP server 有沒有被執行起來 (這點也很重要,不然 LXC 的網路就無法正常運作):

$ ps aux | grep lxc-dnsmasq
dnsmasq       1640  0.0  0.0  26080   900 ?        S    10:13   0:00 dnsmasq -u lxc-dnsmasq --strict-order --bind-interfaces --pid-file=/var/run/lxc/dnsmasq.pid --conf-file= --listen-address 10.0.3.1 --dhcp-range 10.0.3.2,10.0.3.254 --dhcp-lease-max=253 --dhcp-no-override --except-interface=lo --interface=lxcbr0 --dhcp-leasefile=/var/lib/misc/dnsmasq.lxcbr0.leases --dhcp-authoritative
user    12013  0.0  0.0  13660   952 pts/2    S+   15:47   0:00 grep --color=auto lxc-dnsmasq

接下來就可以產生 Juju 的設定檔了:

$ juju init 
A boilerplate environment configuration file has been written to /home/user/.juju/environments.yaml.
Edit the file to configure your juju environment and run bootstrap.

然後修改一下 ~/.juju/environments.yaml 將 "default: amazon" 改成 "default: local",接下來就可以執行 `sudo juju bootstrap` 來產生 Local Provider 所需要的環境。

`sudo juju bootstrap` 所做的動作就是產生 ~/.juju/local-cert.pem 及 ~/.juju/local-private-key.pem 還有 /etc/init/juju-*.conf 的檔案來執行一個 MongoDB Server 給 Juju 使用。

接下來執行 Ubuntu Juju 的資料就會全部儲存在 ~/.juju/local/ 目錄底下了。

$ sudo tree .juju                                                                                                                                                                              
.juju
├── charmcache
│   ├── cs_3a_precise_2f_mysql-27.charm
│   └── cs_3a_precise_2f_wordpress-16.charm
├── environments.yaml
├── local
│   ├── agents
│   │   └── machine-0
│   │       └── agent.conf
│   ├── db
│   │   ├── admin.0
│   │   ├── admin.ns
│   │   ├── journal
│   │   │   ├── j._0
│   │   │   ├── lsn
│   │   │   ├── prealloc.1
│   │   │   └── prealloc.2
│   │   ├── juju.0
│   │   ├── juju.ns
│   │   ├── mongod.lock
│   │   ├── presence.0
│   │   ├── presence.ns
│   │   └── _tmp
│   ├── log
│   │   └── machine-0.log
│   ├── server.pem
│   ├── shared-storage
│   ├── storage
│   │   ├── bootstrap-verify
│   │   ├── provider-state
│   │   └── tools
│   │       ├── juju-1.14.0.1-precise-amd64.tgz
│   │       └── juju-1.14.0.1-raring-amd64.tgz
│   └── tools
│       └── 1.14.0.1-raring-amd64
│           ├── downloaded-url.txt
│           ├── FORCE-VERSION
│           └── jujud
├── local-cert.pem
└── local-private-key.pem

13 directories, 26 files

像是如果要架設一台 WordPress Blog Server 就只需要執行以下四行指令:

$ juju deploy wordpress
$ juju deploy mysql
$ juju add-relation wordpress mysql
$ juju expose wordpress

因為建構這些服務需要一些時間去網路上抓取資料回來安裝設定,所以馬上執行 `juju status` 會看到以下輸出,表示動作還沒有完成:

$ juju status
environment: local
machines:
  "0":
    agent-state: started
    agent-version: 1.14.0.1
    dns-name: 10.0.3.1
    instance-id: localhost
    series: raring
  "1":
    instance-id: pending
    series: precise
  "2":
    instance-id: pending
    series: precise
services:
  mysql:
    charm: cs:precise/mysql-27
    exposed: false
    relations:
      cluster:
      - mysql
      db:
      - wordpress
    units:
      mysql/0:
        agent-state: pending
        machine: "2"
  wordpress:
    charm: cs:precise/wordpress-16
    exposed: true
    relations:
      db:
      - mysql
      loadbalancer:
      - wordpress
    units:
      wordpress/0:
        agent-state: pending
        machine: "1"

過一會後再執行 `juju status` 就可以看到全部完成的結果了。

$ juju status                                                                                                                                                                     
environment: local
machines:
  "0":
    agent-state: started
    agent-version: 1.14.0.1
    dns-name: 10.0.3.1
    instance-id: localhost
    series: raring
  "1":
    agent-state: started
    agent-version: 1.14.0.1
    instance-id: user-local-machine-1
    instance-state: missing
    series: precise
  "2":
    agent-state: started
    agent-version: 1.14.0.1
    instance-id: user-local-machine-2
    instance-state: missing
    series: precise
services:
  mysql:
    charm: cs:precise/mysql-27
    exposed: false
    relations:
      cluster:
      - mysql
      db:
      - wordpress
    units:
      mysql/0:
        agent-state: started
        agent-version: 1.14.0.1
        machine: "2"
        public-address: 10.0.3.158
  wordpress:
    charm: cs:precise/wordpress-16
    exposed: true
    relations:
      db:
      - mysql
      loadbalancer:
      - wordpress
    units:
      wordpress/0:
        agent-state: started
        agent-version: 1.14.0.1
        machine: "1"
        open-ports:
        - 80/tcp
        public-address: 10.0.3.69

然後就可以連到 10.0.3.69 這個 IP 就可以開始使用 WordPress 了。 :-)

如果想要清掉這些服務,只要執行下面一行指令:

$ sudo juju destroy-environment
WARNING: this command will destroy the "local" environment (type: local)
This includes all machines, services, data and other resources.

Continue [y/N]? y

這樣就會將之前用 `sudo juju bootstrap` 產生的環境整個清除乾淨。

$ tree .juju
.juju
├── charmcache
│   ├── cs_3a_precise_2f_mysql-27.charm
│   └── cs_3a_precise_2f_wordpress-16.charm
├── environments.yaml
├── local-cert.pem
└── local-private-key.pem

1 directory, 5 files

x11-touchscreen-calibrator 自動校正觸控螢幕的小程式

現在的筆記型電腦很多都搭配著觸控螢幕的功能,在 X Window System 上面使用時會遇到一些問題,像是接上外接螢幕後、或是變更螢幕解析度後,使用觸控螢幕時游標不再跟著手指頭觸控到的地方了。

可是我找來找去都沒有可以自動校正的應用程式,於是就開始了這個小小的專案。

專案首頁在 http://fourdollars.github.io/x11-touchscreen-calibrator/

目前這個程式已經放到 Debian 官方套件庫裡面了,也準備了一個 Ubuntu PPA 來使用。

在 Debian/Ubuntu 上面使用方式很簡單,就是安裝它後重新啟動 X Window System 就會自動在背景執行校正動作。

如果有使用上的問題請回報到 https://github.com/fourdollars/x11-touchscreen-calibrator/issues

2013年8月28日 星期三

在 Ubuntu 上面 Linux Container 的使用心得

Linux Container (以下簡稱為 lxc) 是一個輕量級的虛擬化系統,介於 VirtualBox/VMWare/... 以及打過類固醇的 chroot 之間,簡單來說 lxc 建立了一個 chroot 的環境,但是這個 chroot 的環境卻可以做資源控制,像是 CPU 用量、硬碟空間、記憶體空間、網路還有其它資源等等。

我們可以在上面安裝其它 Linux Distribution 的環境,看看 /usr/share/lxc/templates 這個目錄底下有支援 Arch, Debian, Fedora, openSUSE, Ubuntu 還有其它。

lxc 也可以用來建立雲端作業環境,又或者用來玩 Steam(TM) 上面的遊戲。

參考 LXC LXC 這兩份線上文件也許是個不錯的入門起點。

在 Ubuntu 上面使用只要安裝 lxc 這個軟體套件就可以了。

sudo apt-get install lxc

如果是在 Ubuntu 12.04 上面,建議使用 precise-backports 裡面的 lxc 效果會比較好,如果想要在 x86 上面建立 arm 的虛擬環境就要借助安裝使用 qemu-user-static 這個軟體套件。

sudo apt-get install qemu-user-static

在使用 lxc 之前可以先執行 `lxc-checkconfig` 來檢查環境。

然後就可以開始建立一個 lxc 的實體了,例如想要建立 Ubuntu 12.04 i386 就可以執行以下指令。

sudo lxc-create -t ubuntu -n myLXC -- -r precise -a i386

當中的 precise 可以換成 quantal/raring/saucy/... 而 i386 則可以換成 amd64/armel/armhf/... 然後 myLXC 則是這個 lxc 實體的名稱。

等待一段時間後,等到它建立完成就可以執行以下指令在背景啟動它。

sudo lxc-start -n myLXC -d

如果想要登入就可以執行:

sudo lxc-console -n myLXC

如果想要離開就要使用特殊的指令組合 <Ctrl+a q>

`sudo lxc-list`
可以看到每個 lxc 的情況。
`sudo lxc-stop -n myLXC`
停止 myLXC 這個 lxc 實體。
`sudo lxc-destroy -n myLXC`
刪除 myLXC 這個 lxc 實體。
`sudo lxc-start-ephemeral -o myLXC -d`
從 myLXC 建立並啟動一個暫時用完即丟的 lxc 實體

大概就是這樣。:-)

P.S. 執行 `sudo SUITE=sid MIRROR=http://ftp.tw.debian.org/debian/ lxc-create -t debian -n sid` 可以用來建立一個 Debian sid 的環境。

2013年8月26日 星期一

使用 git-buildpackage 維護原本就是使用 Git 維護的上游軟體的 Debian package

首先是將上游軟體的原始碼庫抓回來。

git clone https://github.com/fourdollars/x11-touchscreen-calibrator.git

然後是進到該原始碼目錄底下匯入已經釋出的 tarball

git-import-orig --upstream-vcs-tag=0.0 --upstream-branch=master --pristine-tar ../x11-touchscreen-calibrator_0.0.orig.tar.xz

上面這個指令會建立一個 commit 將 tarball 的內容 import 進去,並且建立一個 upstream/0.0 的 tag,只不過 master 也會指過去,所以再執行以下的指令倒回。

git reset --hard HEAD^

接下來是開始製作 Debian package 的部份,先建立一個 debian 的分支。

git checkout -b debian

然後在將 debian/ 這個目錄所需要的檔案都準備好,最後再 commit,然後就可以使用下面的指令來產生 Debian source package。

git-buildpackage --git-pristine-tar --git-debian-branch=debian -S --lintian-opts --profile debian

上面指令中的 `--lintian-opts --profile debian` 是在 Ubuntu 上面才需要加入的,如果在 Debian 裡面應該不需要使用。

另外,在還沒有完全準備好 debian/ 底下的檔案之前,也可以加入使用 `--git-ignore-new` 先試試看產生出來的 Debian source package 的品質如何,再決定要不要 commit。

等到這個 Debian package 已經成功被放進 Debian 官方套件庫裡面,就可以執行以下指令建立 debian/0.0-1 的 tag。

git-buildpackage --git-tag --git-pristine-tar --git-debian-branch=debian -S --lintian-opts --profile debian

最後就可以執行

git push --tags
將所有的 commit 跟 tag 都送回原本的 Git Repo 裡面。

參考文件:

2013年8月7日 星期三

使用 GDB 追縱執行中程式裡面的變數值

首先是要安裝 debug symbols 的套件來使用。

Debian 可以參考 HowToGetABacktrace - Debian Wiki
Ubuntu 可以參考 DebuggingProgramCrash - Ubuntu Wiki

例如目標是 Ubuntu 12.04 中的 Xorg 裡面的 positionSprite() 函式在執行時傳入的變數值,所以先用

$ ps aux | grep X
root      1044  0.4  1.0 127440 21524 tty7     Ss+  18:44   0:06 /usr/bin/X :0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch -background none
u         2633  0.0  0.0  13616   928 pts/2    S+   19:11   0:00 grep --color=auto X
來找出 Xorg 的 PID

然後執行以下指令:

$ sudo gdb /usr/bin/Xorg 1044                                                                                                                                                                         
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://bugs.launchpad.net/gdb-linaro/>...
Reading symbols from /usr/bin/Xorg...Reading symbols from /usr/lib/debug/usr/bin/Xorg...done.
done.
Attaching to program: /usr/bin/Xorg, process 1044
Reading symbols from /lib/x86_64-linux-gnu/libudev.so.0...(no debugging symbols found)...done.
Loaded symbols for /lib/x86_64-linux-gnu/libudev.so.0
... [省略]
(gdb) set height 0
(gdb) break positionSprite
Breakpoint 1 at 0x7fedc71d1140: file ../../dix/getevents.c, line 940.
(gdb) c
Continuing.

Breakpoint 1, positionSprite (dev=0x7fedc94105d0, mode=0, mask=0x7fff3c6e6780, devx=0x7fff3c6e6770, devy=0x7fff3c6e6778, screenx=0x7fff3c6e6760, screeny=0x7fff3c6e6768) at ../../dix/getevents.c:940
940     ../../dix/getevents.c: 沒有此一檔案或目錄.
(gdb) commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>print *devx
>print *devy
>print *screenx
>print *screeny
>continue
>end
(gdb) c
Continuing.

接下來移動滑鼠就可以看到以下的訊息。

Breakpoint 1, positionSprite (dev=0x7fedc94105d0, mode=0, mask=0x7fff3c6e6780, devx=0x7fff3c6e6770, devy=0x7fff3c6e6778, screenx=0x7fff3c6e6760, screeny=0x7fff3c6e6768) at ../../dix/getevents.c:940
940     in ../../dix/getevents.c
$45 = 1023.4510981145139
$46 = 757.60130195816362
$47 = 1366.6023460649326
$48 = 758.58904811456273

Breakpoint 1, positionSprite (dev=0x7fedc94105d0, mode=0, mask=0x7fff3c6e6780, devx=0x7fff3c6e6770, devy=0x7fff3c6e6778, screenx=0x7fff3c6e6760, screeny=0x7fff3c6e6768) at ../../dix/getevents.c:940
940     in ../../dix/getevents.c
$49 = 1022.651098102593
$50 = 757.60130195816362
$51 = 1365.5341153549775
$52 = 758.58904811456273

可以將設定好的 breakpoints 的動作儲存成一個腳本檔

(gdb) save breakpoints break.cmd
Saved to file 'break.cmd'.

腳本的內容是:

set height 0
break positionSprite
  commands
    print *devx
    print *devy
    print *screenx
    print *screeny
    cont
  end

只是第一行的 set height 0 是手動加上去的,目的是不要訊息過長就停下來。

下次再使用 GDB 時就可以執行 source 載入使用。

(gdb) source break.cmd 
Breakpoint 1 at 0x7fedc71d1140: file ../../dix/getevents.c, line 940.
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00007fedc71d1140 in positionSprite at ../../dix/getevents.c:940
        print *devx
        print *devy
        print *screenx
        print *screeny
        cont

只是前面有一個找不到原始碼的小問題,可以執行以下指令來下載原始碼:

$ apt-get source xserver-xorg-core-lts-quantal
或是:
$ pull-lp-source xorg-server-lts-quantal precise

然後再執行:

$ sudo gdb /usr/bin/Xorg 1044 -d xorg-server-lts-quantal-1.13.0

最後 GDB 有其它的 Frontend 可以使用,像是 cgdb 以及 ddd 在這裡就不多提了。

2013年8月5日 星期一

COSCUP 2013 第一天晚上的 Key Signing Party 之後要做的事情

大致上需要的工具在以前的文章就有提到了,所以先參考 利用 caff 來加速 GPG sign 的過程加入 GnuPG 的信任簽章進自己的金鑰圈裡 以及 利用 ssmtp 透過 Gmail 在文字模式下寄信

然後就是以下的四大步驟:

一、首先是簽署所有信任的簽章寄給對方
$ caff 3860D2A5 CEC6AD46 FEBFB7FE 3D058888 54D0F048 642EC1C5 24FF2AE5 0914A01A 03F4552D B6A93879 012596EC 3C728A5D 4A293CBD 35D0CEF2 206AFB69 F37883B4 E9D4AEB6 1478504E 524E09E7
二、然後是匯入所有人的簽章
$ gpg --recv-keys 3860D2A5 CEC6AD46 FEBFB7FE 3D058888 54D0F048 642EC1C5 24FF2AE5 0914A01A 03F4552D B6A93879 012596EC 3C728A5D 4A293CBD 35D0CEF2 206AFB69 F37883B4 E9D4AEB6 1478504E 524E09E7
三、再來是匯入別人對我簽署過的簽章

因為我用的是 Gmail 所以我是執行

$ gpg -d | gpg --import
然後從 Gmail 上面來一個一個的複製貼上 (過程中需要按下 Ctrl-D 產生一個 EOF 來完成)

四、最後就是將別人對我的信任簽章上傳到網路上
gpg --send-keys E9EC46F5A547F31E

最後的三跟四步驟就是等別人寄他的信任簽章過來,再不斷地重覆上傳到網路上。

2013年7月31日 星期三

Linux 電源供應的系統資訊

在筆記型電腦或是使用藍牙裝置時,系統上面都會有電源供應的資訊。

只要在 /sys/devices 底下找尋 power_supply 的目錄就可以找到。

如下所示:

u@u:/sys/devices$ find -name power_supply | while read path; do ls -l $(dirname $path); done
total 0
lrwxrwxrwx 1 root root    0 Jul 31 17:42 driver -> ../../../../../../../../../../bus/hid/drivers/apple
drwxr-xr-x 3 root root    0 Jul 31 17:38 hidraw
-r--r--r-- 1 root root 4096 Jul 31 17:42 modalias
drwxr-xr-x 2 root root    0 Jul 31 17:39 power
drwxr-xr-x 3 root root    0 Jul 31 17:39 power_supply
-r--r--r-- 1 root root 4096 Jul 31 17:42 report_descriptor
lrwxrwxrwx 1 root root    0 Jul 31 17:38 subsystem -> ../../../../../../../../../../bus/hid
-rw-r--r-- 1 root root 4096 Jul 31 17:38 uevent
total 0
lrwxrwxrwx 1 root root    0 Jul 31 17:42 driver -> ../../../../../../../../../../bus/hid/drivers/magicmouse
drwxr-xr-x 3 root root    0 Jul 31 17:39 hidraw
-r--r--r-- 1 root root 4096 Jul 31 17:42 modalias
drwxr-xr-x 2 root root    0 Jul 31 17:39 power
drwxr-xr-x 3 root root    0 Jul 31 17:39 power_supply
-r--r--r-- 1 root root 4096 Jul 31 17:42 report_descriptor
lrwxrwxrwx 1 root root    0 Jul 31 17:39 subsystem -> ../../../../../../../../../../bus/hid
-rw-r--r-- 1 root root 4096 Jul 31 17:39 uevent
total 0
lrwxrwxrwx 1 root root    0 Jul 31 17:42 driver -> ../../../../../../bus/acpi/drivers/battery
-r--r--r-- 1 root root 4096 Jul 31 17:42 hid
-r--r--r-- 1 root root 4096 Jul 31 17:42 modalias
-r--r--r-- 1 root root 4096 Jul 31 17:42 path
drwxr-xr-x 2 root root    0 Jul 31 17:39 power
drwxr-xr-x 3 root root    0 Jul 31 17:36 power_supply
lrwxrwxrwx 1 root root    0 Jul 31 17:36 subsystem -> ../../../../../../bus/acpi
-rw-r--r-- 1 root root 4096 Jul 31 17:36 uevent
total 0
lrwxrwxrwx 1 root root    0 Jul 31 17:42 driver -> ../../../../../../bus/acpi/drivers/ac
-r--r--r-- 1 root root 4096 Jul 31 17:42 hid
-r--r--r-- 1 root root 4096 Jul 31 17:42 modalias
-r--r--r-- 1 root root 4096 Jul 31 17:42 path
drwxr-xr-x 2 root root    0 Jul 31 17:39 power
drwxr-xr-x 3 root root    0 Jul 31 17:36 power_supply
lrwxrwxrwx 1 root root    0 Jul 31 17:36 subsystem -> ../../../../../../bus/acpi
-rw-r--r-- 1 root root 4096 Jul 31 17:36 uevent

所以可以從 driver 這個 symbolic link 看出每個電源供應是屬於哪些裝置。

另外也可以使用指令來監測 udev 裡面關於 power_supply 的訊息。

如下所示:

u@u:~$ udevadm monitor --subsystem-match=power_supply                                                                                                                                                      
monitor will print the received events for:
UDEV - the event which udev sends out after rule processing
KERNEL - the kernel uevent

KERNEL[65.358427] change   /devices/pci0000:00/0000:00:1a.0/usb3/3-1/3-1:1.0/bluetooth/hci0/hci0:13/0005:05AC:030D.0002/power_supply/hid-AB:CD:EF:12:34:56-battery (power_supply)
UDEV  [65.398449] change   /devices/pci0000:00/0000:00:1a.0/usb3/3-1/3-1:1.0/bluetooth/hci0/hci0:13/0005:05AC:030D.0002/power_supply/hid-AB:CD:EF:12:34:56-battery (power_supply)
KERNEL[81.032204] change   /devices/LNXSYSTM:00/device:00/PNP0A08:00/device:0c/PNP0C0A:00/power_supply/BAT1 (power_supply)
UDEV  [81.033615] change   /devices/LNXSYSTM:00/device:00/PNP0A08:00/device:0c/PNP0C0A:00/power_supply/BAT1 (power_supply)

最後就是 Freedesktop 有一個 UPower 專案,專門用來提供底層電源資訊的介面給桌面應用程式來使用。

如果是在 GNOME 桌面環境底下就是 gnome-settings-daemon 與 gnome-power-manager 透過 DBus 來使用 UPower 以提供圖形介面給桌面使用者使用。

2013年7月22日 星期一

GRUB2 的除錯訊息

GRUB2 的除錯很麻煩,因為有些問題只會在特定硬體上發生,所以無法使用其它軟體模擬的方式進行,不過還好 GRUB2 本身就內建一個除錯訊息的環境變數就叫做 debug

在 Debian/Ubuntu 底下可以直接修改 /boot/grub/grub.cfg 在最前面加上 set debug=all 就可以在執行 GRUB2 時將所有的除錯訊息都列印在電腦螢幕上面,不過更好的方式是在 /etc/grub.d/ 底下建立一個 shell script 來 echo 出 set debug=all

只是這種作法只能使用在 GRUB2 去硬碟上面讀取 /boot/grub/grub.cfg 之後的除錯訊息,另外就是除錯訊息本身太多太雜,反而會有一種見林不見樹的感覺。

所以這個時候可以使用一些小技巧將 debug 訊息的分類 (category) 都找出來,然後去掉不想要的部份再使用在 debug 參數設定上面。

例如我們可以在 GRUB2 的原始碼裡面執行以下指令:

~/grub2-1.99$ grep grub_dprintf * -Irh | sed 's/[[:space:]]\+//g' | grep '^grub_dprintf("' | cut -d '"' -f 2 | sort -u | grep -v scripting | grep -v lexer | xargs echo | sed 's/ /,/g'
acpi,affs,ata,atkeyb,badram,bsd,btrfs,datetime,devalias,deviceiter,disk,drivemap,efi,efidisk,efiemu,elf,expand,fat,fb,fs,gettext,gpt,hostdisk,keystatus,linux,loader,memdisk,mmap,modules,multiboot_loader,ohci,partition,play,raid,reiserfs,reiserfs_blocktype,reiserfs_tree,relocator,scsi,uhci,usb,usb_keyboard,usbms,video,xnu,zfs

( 截取的方法也有很多可以參考此噗的討論 )

上面的指令將 scripting 跟 lexer 這兩個最囉唆的分類給去掉了,所以 GRUB2 的設定變成 set debug=acpi,affs,ata,atkeyb,badram,bsd,btrfs,datetime,devalias,deviceiter,disk,drivemap,efi,efidisk,efiemu,elf,expand,fat,fb,fs,gettext,gpt,hostdisk,keystatus,linux,loader,memdisk,mmap,modules,multiboot_loader,ohci,partition,play,raid,reiserfs,reiserfs_blocktype,reiserfs_tree,relocator,scsi,uhci,usb,usb_keyboard,usbms,video,xnu,zfs 就可以少掉很多訊息了。

另外就是去修改 GRUB2 原始碼加上自己的除錯訊息,像是:

grub_dprintf ("debug", "Your debug messages here\n");

然後重編 GRUB2 再安裝到系統上,再使用 set debug=debug 或是加到原本那一串長長的清單後面。

最後,linux 這個分類算是從 GRUB2 進入 Linux kernel 最重要的點,所以可以使用 set debug=linux 就可以看出系統是停在 GRUB2 裡面還是死在 Linux kernel 裡面。

還有最後的最後就是要將 /etc/default/grub 裡面的 GRUB_TERMINAL=console 打開,再執行 update-grub 才會有比較容易閱讀的除錯訊息,當然也許會有一些 Side Effect 的產生。

2013年6月7日 星期五

在 Ubuntu 的 Node.js 使用初體驗

安裝及環境設定

首先要安裝 Node.js,由於 Node.js 是一個快速發展中的網頁框架,Ubuntu 內建的 Node.js 版本當然就很容易老舊過時,還好官方準備了 Debian/Ubuntu 的套件庫可以直接使用。

curl -sL https://deb.nodesource.com/setup_5.x | sudo -E bash -
sudo apt-get install -y nodejs

建立專案

接下來是使用 npm 手動建立一個 Node.js 的專案,照著指示做就可以了。

$ mkdir myapp
$ cd myapp
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install  --save` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (myapp) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /home/sylee/myapp/package.json:

{
  "name": "myapp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this ok? (yes)

接下來照著 Node.js 官方網站上的第一個範例建立一個簡單的 HTTP Server。

var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

將以上的內容寫入 server.js 然後修改 package.json 在 "scripts" 裡面加入 "start": "node server.js"。

{
  "name": "myapp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

然後就可以使用 npm start 來啟動 Server 啦。


> myapp@0.0.0 start /home/sylee/myapp
> node server.js

Server running at http://127.0.0.1:1337/

使用模組

如果說要整個重頭寫 HTTP Server 提供所有的內容一定是很吃力的,所以當然就是要使用別人寫好的 node module 來做快速開發。

接下來要來使用 Express.js 首先是安裝它。

$ npm install --save express

然後就可以將先前的 server.js 改寫。

var express = require('express');
var app = express();
app.get('/hello.txt', function(req, res){
  var body = 'Hello World';
  res.setHeader('Content-Type', 'text/plain');
  res.setHeader('Content-Length', body.length);
  res.end(body);
});
app.listen(3000);
console.log('Listening on port 3000');

然後重新執行 npm start 啟動 Server 就可以在 http://localhost:3000/hello.txt 看到結果了。

這個例子與先前的例子之間的差別是: http://127.0.0.1:1337 底下無論任何的路徑 http://127.0.0.1:1337/foo http://127.0.0.1:1337/bar 都是看到一樣的結果,而 http://localhost:3000 就只有 http://localhost:3000/hello.txt 可以看到 Hello World 其它的路徑就只能看到 Cannot GET balabala

2013年5月24日 星期五

Bash/Dash Shell Script 當中的邏輯判斷

首先是函式的例子:
#!/bin/sh

yes_func()
{
    echo yes_func
    true
}

no_func()
{
    echo no_func
    false
}
                                
if ! yes_func || yes_func; then
    echo two yes_func
fi

if ! no_func && ! no_func; then
    echo two no_func
fi

驚歎號 '!' 是真假值反相的運算,要與函式名稱之間用空白隔開,否則會成為函式名稱的一部份。

另外就是「且」邏輯運算 '&&' 與「或」邏輯運算 '||' 的執行優先序都比反相邏輯運算 '!' 還要低。

接下來是數值的例子:
#!/bin/sh

NUM=5

if [ $NUM -eq 5 ]; then
    echo "#$NUM == 5"
else
    echo "#$NUM != 5"
fi

if [ $NUM -ne 5 ]; then
    echo "#$NUM != 5"
else
    echo "#$NUM == 5"                     
fi

if [ $NUM -gt 0 -a $NUM -lt 10 ]; then
    echo "0 < #$NUM < 10"
fi

NUM=11

if [ ! $NUM -gt 0 -o ! $NUM -lt 10 ]; then
    echo "#$NUM <= 0 or #$NUM >= 10"
fi

if [ $NUM -le 0 -o $NUM -ge 10 ]; then
    echo "#$NUM <= 0 or #$NUM >= 10"
fi

if [ ! $NUM -lt 0 -a $NUM -gt 10 ]; then
    echo "#$NUM >= 0 and #$NUM > 10"
fi

if [ $NUM -ge 0 -a $NUM -gt 10 ]; then
    echo "#$NUM >= 0 and #$NUM > 10"
fi

數值的算數運算子有:

  • '-eq' 等於 '=='
  • '-ne' 不等於 '!='
  • '-lt' 小於 '<'
  • '-gt' 大於 '>'
  • '-le' 小於或等於 '<='
  • '-ge' 大於或等於 '>='

而 '-a' 相當於前面提過的「且」邏輯運算 '&&','-o' 則是相當於「或」邏輯運算 '||',同樣地優先序都比反相邏輯運算 '!' 還要低。

最後是是否為空字串的例子:
#!/bin/sh

STR1="foo"
STR2=""

if [ ! -z "$STR1" -a -z "$STR2" ]; then
    echo "'$STR1' is not empty, and '$STR2' is empty."
fi

if [ -n "$STR1" -a -z "$STR2" ]; then
    echo "'$STR1' is not empty, and '$STR2' is empty."
fi

if [ -z "$STR1" -o -z "$STR2" ]; then
    echo "'$STR1' is empty, or '$STR2' is empty."
fi

STR1="foo"
STR2="bar"

if [ ! -z "$STR1" -a ! -z "$STR2" ]; then
    echo "'$STR1' is not empty, and '$STR2' is not empty."
fi

STR1=""
STR2=""

if ! [ -n "$STR1" -o -n "$STR2" ]; then
    echo "'$STR1' is empty, and '$STR2' is empty."
fi

if [ -z "$STR1" -a -z "$STR2" ]; then
    echo "'$STR1' is empty, and '$STR2' is empty."
fi                                                        

'-z' 是否為空字串的運算子,'-n' 是否為非空字串的運算子。

跟前面的數值運算一樣都需要用中括號包起來使用,另外就是反相邏輯運算 '!' 可以直接使用在中括號的運算結果。

2013年5月13日 星期一

自製 Ubuntu 13.04/12.10/12.04.2 與 Debian 7.0 測試硬體安裝使用的 USB 隨身碟

為了同時可以在 Legacy BIOS 與 UEFI BIOS 上使用的,首先使用 Ubuntu 12.04.2 以上內建的 usb-creator-gtk 工具來產生一個 Ubuntu 13.04 desktop amd64 的 USB 隨身碟來用。

$ usb-creator-gtk -n -i ubuntu-13.04-desktop-amd64.iso

接下來的動作,只要使用 Ubuntu 13.04 的 Live System (amd64) 就可以了,不用安裝到硬碟上面。

進入 Ubuntu 13.04 Live System (amd64) 之後,執行以下指令。

ubuntu@ubuntu:~$ sudo apt-get install grub-pc-bin grub-efi-amd64-bin                                                                                                                                       
正在讀取套件清單... 完成
正在重建相依關係
正在讀取狀態資料... 完成
grub-pc-bin 已經是最新版本了。
下列【新】套件將會被安裝:
  efibootmgr grub-efi-amd64-bin
升級 0 個,新安裝 2 個,移除 0 個,有 0 個未被升級。
需要下載 0 B/581 kB 的套件檔。
此操作完成之後,會多佔用 2,316 kB 的磁碟空間。
選取了原先未選的套件 efibootmgr。
(讀取資料庫 ... 目前共安裝了 161462 個檔案和目錄。)
解開 efibootmgr(從 .../efibootmgr_0.5.4-4ubuntu1_amd64.deb)...
選取了原先未選的套件 grub-efi-amd64-bin。
解開 grub-efi-amd64-bin(從 .../grub-efi-amd64-bin_2.00-13ubuntu3_amd64.deb)...
執行 man-db 的觸發程式 ...
設定 efibootmgr (0.5.4-4ubuntu1) ...
設定 grub-efi-amd64-bin (2.00-13ubuntu3) ...

將另外一隻 USB 隨身碟插入 USB 埠,執行以下指令。

ubuntu@ubuntu:~$ sudo grub-install --removable --target=i386-pc --root-directory=/media/ubuntu/UsbStick/ /dev/sdc
Installation finished. No error reported.
ubuntu@ubuntu:~$ sudo grub-install --removable --target=x86_64-efi --root-directory=/media/ubuntu/UsbStick/ /dev/sdc
/usr/sbin/grub-probe: error: failed to get canonical path of /cow.
Installation finished. No error reported.

請將以上的 /dev/sdc 與 /media/ubuntu/UsbStick/ 代換成符合實際使用環境上的字串。

接下來將 grub.cfg 放到 /media/ubuntu/UsbStick/boot/grub/ 底下。

最後再將相對應的 ISO 檔案都放到 /media/ubuntu/UsbStick/iso/ 底下就可以了。

之後只要針對 grub.cfg 的內容做調整加入需要的選項即可。

這樣一來就可以在安裝前先測試看看硬體的相容性,再來決定要不要安裝到硬碟裡面。

P.S. Ubuntu 上面有些無線網路的驅動程式是要安裝到硬碟裡面重新啟動才會正常運作的。

2013年3月15日 星期五

關於 Thunderbird 在 IMAP 上使用郵件篩選中的自訂標頭的問題

過去一直很困擾這個問題,不過今天找到了一些解決方法,在此記錄一下。

首先來看 thunderbird-17.0.4+build1/mailnews/imap/src/nsImapProtocol.cpp 中的一段程式碼

// read in the accept languages preference
if (prefBranch)
{
  if (!gInitialized)
    GlobalInitialization(prefBranch);

  nsCOMPtr prefString;
  prefBranch->GetComplexValue("intl.accept_languages",
                              NS_GET_IID(nsIPrefLocalizedString),
                              getter_AddRefs(prefString));
  if (prefString)
    prefString->ToString(getter_Copies(mAcceptLanguages));

  nsCString customDBHeaders;
  prefBranch->GetCharPref("mailnews.customDBHeaders",
                          getter_Copies(customDBHeaders));

  ParseString(customDBHeaders, ' ', mCustomDBHeaders);                             
  prefBranch->GetBoolPref("mailnews.display.prefer_plaintext", &m_preferPlainText);
}

這段程式碼裡面使用的是 mailnews.customDBHeaders 但是在使用自訂標頭後是卻是儲存在 mailnews.customHeaders

然後再看一下同一個檔案中的另外一段程式碼

if (!downloadAllHeaders)  // if it's ok -- no filters on any header, etc.
{                                                                                        
  char *headersToDL = nullptr;
  char *what = nullptr;
  const char *dbHeaders = (gUseEnvelopeCmd) ? IMAP_DB_HEADERS : IMAP_ENV_AND_DB_HEADERS;
  nsCString arbitraryHeaders;
  GetArbitraryHeadersToDownload(arbitraryHeaders);
  for (uint32_t i = 0; i < mCustomDBHeaders.Length(); i++)
  {
    if (arbitraryHeaders.Find(mCustomDBHeaders[i], CaseInsensitiveCompare) == kNotFound)
    {
      if (!arbitraryHeaders.IsEmpty())
        arbitraryHeaders.Append(' ');
      arbitraryHeaders.Append(mCustomDBHeaders[i]);
    }
  }
  if (arbitraryHeaders.IsEmpty())
    headersToDL = strdup(dbHeaders);
  else
    headersToDL = PR_smprintf("%s %s",dbHeaders, arbitraryHeaders.get());

  if (gUseEnvelopeCmd)
    what = PR_smprintf(" ENVELOPE BODY.PEEK[HEADER.FIELDS (%s)])", headersToDL);
  else
    what = PR_smprintf(" BODY.PEEK[HEADER.FIELDS (%s)])",headersToDL);
  NS_Free(headersToDL);
  if (what)
  {
    commandString.Append(" %s (UID ");
     if (m_isGmailServer)
      commandString.Append("X-GM-MSGID X-GM-THRID X-GM-LABELS ");
    if (aolImapServer)
      commandString.Append(" XAOL.SIZE") ;
    else
      commandString.Append("RFC822.SIZE");
    commandString.Append(" FLAGS");
    commandString.Append(what);
    PR_Free(what);
  }
  else
  {
    commandString.Append(" %s (UID RFC822.SIZE BODY.PEEK[HEADER] FLAGS)");
  }
}

搭配前一段程式碼一起來看,應該是使用空白當分隔符號來進行不分大小寫的比對,於是我就手動將 mailnews.customHeaders 的內容複製到 mailnews.customDBHeaders 裡面並且去掉了冒號,接著重新啟動 Thunderbird 了。

不過卻發現收件匣當中的郵件還是無法篩選到,於是就注意到前面第二段程式碼最前面的 downloadAllHeaders,想說會不會是因為 IMAP 的關係,之前已經儲存在本地端的郵件的 DB 並沒有儲存這些資訊,應該要設法再讓 Thunderbird 重新抓取郵件的標頭, 於是我就將郵件移往其它的目錄再做篩選,果然就可以正常運作了。

另外我找到了一個似乎有相關的修正 Bug 363238 - saved searches fail for searches on x-headers, r=irving,只是目前它並不存在 Thunderbird 裡面就是了。

2013年3月4日 星期一

關於 Bash 的 Redirection 使用的心得

一般常見的方式是把標準輸出 (stdout) 訊息跟標準錯誤 (stderr) 訊息全部都導進一個檔案裡面,例如以下指令:

$ sudo fdisk -l >fdisk.log 2>&1

如果是寫在一個 Shell Script 的檔案裡面,就可以寫成像是下面這樣:

#!/bin/sh

exec >fdisk.log # 0 (stdin) 1 (fdisk.log) 2 (stderr)
exec 2>&1       # 0 (stdin) 1 (fdisk.log) 2 (fdisk.log)

sudo fdisk -l

但是其實我們也可以使用更複雜的 Redirection,從 bash need STDOUT+STDERR in log, and STDERR to its normal destination 改出來的例子:

$ ((sudo fdisk -l 3>&1 1>&2 2>&3- | tee /dev/fd/2) 3>&1 1>&2 2>&3-) | cat >fdisk.log

這個例子的結果是將標準輸出訊息跟標準錯誤訊息都導進 fdisk.log 裡面,然後將標準錯誤訊息顯示出來。

但是它做了什麼事情?

首先是

sudo fdisk -l 3>&1 1>&2 2>&3-

如果直接用 Shell Script 檔案來寫,會看起來像是這樣:

#!/bin/sh

exec 3>&1  # 0 (stdin) 1 (stdout) 2 (stderr) 3 (stdout)
exec 1>&2  # 0 (stdin) 1 (stderr) 2 (stderr) 3 (stdout)
exec 2>&3- # 0 (stdin) 1 (stderr) 2 (stdout) 3 (closed)

sudo fdisk -l

看起來像是

`sudo fdisk -l` --> stdout ` '-> stdout
                |           X
                `-> stderr ' `-> stderr

它將標準輸出訊息跟標準錯誤訊息互換了,接下來

| tee /dev/fd/2
會收到 pipe 符號前面傳來的標準輸出訊息也就是 `sudo fdisk -l` 的 2 (stdout) 會變成 `tee` 的 0 (stdin),然後 `tee` 會將其內容導到 `tee` 的 2 (stderr) 並且顯示在 `tee` 的 1 (stdout) 於是乎 `sudo fdisk -l` 的標準輸出訊息跟標準錯誤訊息都會被導到 stderr 而標準錯誤訊息則被導到 stdout

看起來像是

                                       `tee`
`sudo fdisk -l` --> stdout ` '-> stdout --> stdout
                |           X           `-> stderr
                `-> stderr ' `-> stderr

接下來再經歷一次的標準輸出訊息跟標準錯誤訊息互換

((...) 3>&1 1>&2 2>&3-)

看起來像是

                                       `tee`
`sudo fdisk -l` --> stdout    -> stdout --> stdout ` '-> stdout
                |          ` '          |           X
                |           X           `-> stderr ' `-> stderr
                `-> stderr ' `-> stderr --> stdout

從最後一張圖就可以看出最後的兩個 stdout 分別來自於 `sudo fdisk -l` 的 stdout 跟 stderr 而最後的一個 stderr 還是來自於 `sudo fdisk -l` 的 stderr

最後一步就是透過 `| cat >fdisk.log` 將兩個 stdout 都導進 fdisk.log 檔案就大功告成啦!:D