Corredor

ウェブ、プログラミングの勉強メモ。

リポジトリごとの GitHub Pages でルート相対パスを使うには

GitHub Pages でルート相対パスを使う時の荒業

ルート相対パスとは?

HTML から画像やスタイルシートなどの外部ファイルを指定する時のパスの書き方は、以下の3つがある。

  1. 絶対パス : https://USER-NAME.github.io/SUB-REPO/style.csshttps://USER-NAME.github.io/scripts.js
    • http:// などのプロトコルから表記する。呼び出し元の HTML がどこに配置されようと、目的の外部ファイルを取得できる
  2. 相対パス : style.css (= ./style.css)・../scripts.js
    • 呼び出し元の HTML のパスから見て相対的な位置を ./ (同階層) ないしは ../ (一階層上) で指定する
  3. ルート相対パス (Root Relative Path) : /SUB-REPO/style.css/scripts.js
    • 呼び出し元の HTML から見たルート / を起点としてパスを指定する (ドメイン部分を省略した絶対パスといえる)
    • 呼び方は「ルートパス」「サイトルートパス (Site-Root Relative Path)」などとも

今回対象にするのは、3つ目のルート相対パス。

ローカルで直接 HTML ファイルを開いてしまうと、ルート相対パスの「ルート」が特定できないため上手く開けないが、何らかのサーバ上で動作させれば、http://localhost:8080/ などをルートとみなして解釈できるようになる、というモノだ。

GitHub Pages におけるルート相対パス

GitHub Pages にデプロイした HTML ファイルに関しても、ルート相対パスが使える。

User Site の場合

USER-NAME.github.io の名前で作ったリポジトリで GitHub Pages を公開する場合、ルートは https://USER-NAME.github.io/ とみなされる。

よって、/index.html から、同階層の styles.css を参照したい場合は、以下のように link 要素が書ける。

<link rel="stylesheet" href="/styles.css">

コレは違和感ないだろう。

Project Site の場合

任意のリポジトリで、よく docs/ ディレクトリを作るか gh-pages ブランチを作るかして公開する GitHub Pages。

URL としては https://USER-NAME.github.io/SUB-REPO/ 配下が当該リポジトリの GitHub Pages となるが、この時のルートは https://USER-NAME.github.io/ とみなされてしまう。

コレはどういうことかというと、任意のリポジトリ SUB-REPO の直下に index.htmlscripts.js を配置し、index.html から以下のようにルート相対パスで参照させようとした場合、

<script src="/scripts.js"></script>

当該リポジトリ直下から探して、

  • https://USER-NAME.github.io/SUB-REPO/scripts.js

を参照して欲しいところだが、実際は一階層上の

  • https://USER-NAME.github.io/scripts.js

を探しに行ってしまうのだ。

プロジェクト・サイトでルート相対パスを使いたい場合は、

<script src="/SUB-REPO/scripts.js"></script>

このようにルート相対パスに当該リポジトリ名を含めてやらないといけない、というワケだ。

…お察しかと思うが、この挙動は大変不便だ。

base 要素でも直せない

「ルート」とみなされるパスを変更する方法はないかと思い、base 要素を用意してみた。

SUB-REPO リポジトリ内で、以下のようなバリエーションで base 要素を記述して検証してみた。

<!-- 絶対パスで SUB-REPO リポジトリまで指定・末尾スラッシュなし -->
<base href="https://USER-NAME.github.io/SUB-REPO">
<!-- 絶対パスで SUB-REPO リポジトリまで指定・末尾スラッシュあり -->
<base href="https://USER-NAME.github.io/SUB-REPO/">

<!-- 相対パスで何かを指定してみる・リポジトリ名を含めるのは嫌だけど… -->
<base href=".">
<base href="./">
<base href="SUB-REPO">
<base href="SUB-REPO/">
<base href="./SUB-REPO">
<base href="./SUB-REPO/">

<!-- ルート相対パスで指定してみる・リポジトリ名を含めるのは嫌だけど… -->
<base href="/">
<base href="/SUB-REPO">
<base href="/SUB-REPO/">

また、検証のため、次のようなリンクを用意した。いずれも、https://USER-NAME.github.io/SUB-REPO/test.html に遷移できれば良いな…と思って書いているモノ。

<!-- 相対パス。挙動を確認するため設置 -->
<a href="test.html">
<a href="./test.html">

<!-- ルート相対パス、こう書いて動作させたい… -->
<a href="/test.html">

<!-- ルート相対パス、リポジトリ名を含むのが嫌… -->
<a href="/SUB-REPO/test.html">

結果から行くと、絶対パスで SUB-REPO リポジトリまで記述した場合しか、上手く行かなかった。どれも思った効果は得られなかった。

まず、base 要素にどのようなパスを書いても、ルート相対パスを書いた a 要素に対しては効果がなかった。

base 要素が影響を及ぼすのは相対パス記述のみで、末尾のスラッシュの有無は関係ない。

概念的には、相対パスの記述に対し、手前に base 要素の値を付与している、というモノのようだ。以下の結果表を見れば少しは想像が付くかもしれない。

base 要素の値 + a 要素などの相対パス値 最終的なパス
https://USER-NAME.github.io / ./test.html https://USER-NAME.github.io/test.html
../ (= https://USER-NAME.github.io/) (/) test.html https://USER-NAME.github.io/test.html
https://USER-NAME.github.io/SUB-REPO/ (/) ./test.html https://USER-NAME.github.io/SUB-REPO/test.html
https://USER-NAME.github.io/SUB-REPO/ (/) ../test.html https://USER-NAME.github.io/test.html

base 要素の値に ../ のような相対パスが指定された場合は、その HTML ファイルのパスを起点に、ベースとするフルパスが導かれる。

Node.js を触った人なら、path.join()path.resolve() のように単に結合しているだけ、と思えば分かりやすいか。

で、ルート相対パスが書かれた場合は、base 要素の値は無視して、サイトルート、この場合は https://USER-NAME.github.io/ をルートと見なす、という動きのようだ。

JavaScript でなんとかしてやろう…

それでも、どうしてもプロジェクトサイトでルート相対パスを使いたい、https://USER-NAME.github.io/SUB-REPO/ のようなドメイン以下の任意のパス以下をルートと見なしたい、ということで、JavaScript で制御する方法を編み出した。

今回はたまたま、SUB-REPO 以下の全ての HTML ファイルが /scripts.js を参照する仕様にしてあった。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/styles.css">
    <script src="/scripts.js"></script>  <!-- ← コレ -->
  </head>
  <body>
    <a href="/test.html"><img src="/image.png"></a>
  </body>
</html>

ルート相対パス /scripts.js は、

  • https://USER-NAME.github.io/scripts.js

と解釈される。プロジェクトサイトではなく、ユーザサイトの領域にある scripts.js を探しに行ってしまうワケだ。

そこで、だ。

ユーザサイトに scripts.js を作成しておき、SUB-REPO の HTML からこの JavaScript ファイルを読み込ませ、そこでルート相対パスを置換することにする。コードの全量は以下。

/** ルート相対パス置換処理 */
(function() {
  /** 定数 : 本ファイルの名前 */
  var thisFileName = 'scripts.js';  // ← ファイル名が異なる場合は変更する
  /** 定数 : 当該 GitHub Pages のルートパス URL を用意する・末尾にスラッシュを付けない */
  var rootPath = 'https://USER-NAME.github.io/SUB-REPO';  // ← ココをリポジトリごとに変更する
  
  // 対象の GitHub Pages から呼び出されていなければ、何も処理せず終了する
  if(location.href.indexOf(rootPath) < 0) {
    return;
  }
  
  /**
   * 指定の要素の属性値をチェックし、ルート相対パス (スラッシュ `/` から始まる値) だった場合、
   * 定数 rootPath を先頭に付与した絶対パスに変換する
   * 
   * @param {string} elementName 要素名
   * @param {string} attributeName 属性名
   */
  var replaceAttribute = function(elementName, attributeName) {
    // console.log(elementName, attributeName, '置換処理開始');
    Array.prototype.forEach.call(document.querySelectorAll(elementName), function(element, index) {
      var attribute = element.getAttribute(attributeName);
      // 属性値がない場合、スラッシュ2つで始まるプロトコル省略の絶対パスの場合、ルート相対パスでない場合は処理しない
      if(!attribute || attribute.substr(0, 2) === '//' || attribute.substr(0, 1) !== '/') {
        return;
      }
      
      if(elementName === 'script' && attribute === thisFileName) {
        // 本ファイル自体は読み込まれているため element は操作しないでおく
        // 代わりに、当該リポジトリ配下にあるはずの同名ファイルを読み込ませるため別要素を作って追加する
        var theScript = document.createElement('script');
        theScript.src = rootPath + attribute;
        element.parentNode.appendChild(theScript);
      }
      else {
        // a 要素・img 要素は属性値変更のみで正しく読み込まれる
        element.setAttribute(attributeName, rootPath + attribute);
        
        // link 要素・script 要素は Node の再挿入を行わないと読込が開始されない
        if(elementName === 'link' || elementName === 'script') {
          var clone = element.cloneNode(true);
          element.parentNode.replaceChild(clone, element);
        }
      }
    });
  };
  
  // 画面のチラつきが発生するため、link 要素のみ本ファイルが読み込まれたタイミングで即処理する
  // (本スクリプトの読み込みは head 要素に書いておくとチラつきが発生しにくくなる)
  replaceAttribute('link', 'href');
  
  /** 初期処理定義 */
  var init = function() {
    replaceAttribute('link', 'href');  // 上の即処理で漏れた CSS ファイルのみ改めて処理する
    replaceAttribute('script', 'src');
    replaceAttribute('a', 'href');
    replaceAttribute('img', 'src');
  };
  
  // 読み込みタイミングに関わらず確実に実行されるよう制御する
  if(!document.readyState || document.readyState === 'interactive') {
    window.addEventListener('load', init);
  }
  else if(document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  }
  else {
    init();
  }
})();

少々長いので順を追って解説する。

  • IE11 でもトランスパイルせず動作するよう書いてある (そのため const は未使用)
  • 最初に2つ定数を定義してある。ルート相対パスの置換処理で必要となる、「本スクリプトファイルの名前」と「ルートと見なしたいパス」の定義だ
  • このスクリプトが「ルートと見なしたいパス」配下の HTML から読み込まれていない場合は、何も処理せず終了する
  • replaceAttribute 関数を定義。ページ中の全ての linkscriptaimg 要素に対応して、ルート相対パスの記述があれば、「ルートと見なしたいパス」を結合して絶対パスに置換する、という処理を定義している
  • a 要素と img 要素については、setAttribute() で値を置換してやれば、すぐに画面上にも反映される。
    しかし、link 要素と script 要素についてはそれだけでは外部ファイルを読み込んでくれず、replaceChild() で DOM を再挿入する必要があった
  • 本来は https://USER-NAME.github.io/SUB-REPO/scripts.js を読み込みたかったのに、ルート相対パスのせいで、この https://USER-NAME.github.io/scripts.js ファイルが読み込まれて、この処理が実行されている。
    ということは、<script src="/scripts.js"> の部分だけは、コレとは別に <script src="https://USER-NAME.github.io/SUB-REPO/scripts.js"> を読み込んで、本来の処理を行わせてあげる必要がある。
    そこで、「本スクリプトファイル名」の script 要素が登場した時だけ、別に script 要素を生成して appendChild() している
  • CSS ファイル (link 要素) は head 要素内で読み込まれるため、本スクリプトが DOMContentLoaded 以降に動作すると、「ページの内容は表示されているのにスタイルが適用されていない」状態が一瞬発生してしまう。その後、本スクリプトによって link 要素のパスが修正され、CSS ファイルが適用されると、結果的に「画面のチラつき」が発生する。
    本スクリプトファイルを head 要素内で読み込んでいれば、その場で link 要素のパスだけ修正してしまい、チラつきを可能な限りなくそうしている
  • その他の要素に関しては、どこでどう呼ばれても実行できるよう、DOMContentLoadedwindow.onloadinit() 関数を予約して処理している。
    このやり方は、読み込みタイミングが本来のタイミングより遅くなる <script src="https://USER-NAME.github.io/SUB-REPO/scripts.js"> にも組み込んでおくと安全だろう

こんな感じ。ハッキリ言ってメチャクチャ強引。

自分が必要なかったので、iframeembedobject 要素などには対応させていないが、replaceAttribute() 関数を拡張すれば対応できると思う。

以上

今回のスクリプトは、自分のメインサイト Neo's World のミラーサイトを GitHub Pages で公開するにあたって編み出した。実際に動作している様子は以下のリンクを覗いてもらえば分かるかと思う。

ということでまとめ。

  • 正規の?方法で、ルート相対パスの起点を変更する方法はなかった (base 要素は効かなかった)
  • スクリプトを読み込むような HTML になっていれば、ユーザサイトの方に同名のスクリプトファイルを置いて、ソイツから「ルート相対パス」を「絶対パス」に置換する処理を行ってやれば、なんとか直せなくもない…

という話でした。

スラスラわかるHTML&CSSのきほん 第2版

スラスラわかるHTML&CSSのきほん 第2版

Go 言語を触ってみる

最近 GitHub で見かけるコマンドラインツールの多くで、PythonGo 言語が採用されているのを見かける。最終的にシングルバイナリにビルドできるっぽくて、OS 間の差異も上手く吸収できそうなので、少し触ってみることにした。

今回はネットで見つけた入門系記事を試してみただけだが、その中でも少しつまづくポイントがあったので、記事にした次第。

Go 言語の開発環境・実行環境の準備

MacOS Mojave の場合は、Homebrew を使ってインストールすると楽。

$ brew install go

Windows の場合は未検証だが、Chocolatey で同じようにインストールできる様子。

> choco install golang

インストールができたら、PATH を通すため ~/.bash_profile 辺りに以下のように追記する。

export GOPATH="${HOME}/go"
export PATH="${GOPATH}/bin:${PATH}"

ターミナル上で $ go version コマンドが通るようになっていれば OK。

# MacOS での例
$ go version
go version go1.12.5 darwin/amd64

コレで Go 言語のファイルをビルドしたり実行したりする基盤はできた。

今回は特別な IDE 等は用意せず、VSCode を使う。特に拡張機能も使わないので、CotEditor や NotePad++ など、簡単なエディタとターミナルツールだけあれば良い。

簡単なコードを書いてみる

まずは最も簡単な、Hello World を出力するだけのプログラムを書いてみる。以下のような内容の hello.go ファイルを作る。

package main

import "fmt"

func main() {
  fmt.Printf("hello, world!")
}

ココでの留意点は以下のとおり。

  • package : このプログラムを外部に公開する際に使用する、パッケージ名を指定する
  • import : 外部ライブラリ等をインポートする。ココではコンソール出力に使う fmt をインポートしている
  • func main() : func で関数宣言。main() 関数は Java などと同じく、自動的に実行されるメイン関数
  • 行末のセミコロンは付けなくて良い。書いても問題ないので、イメージ的には改行がセミコロン代わりになる、JavaScript の言語仕様っぽい感じかな

ということで、コレを実行してみる。

ビルドしてバイナリファイルを作り、それを実行する場合は以下のとおり。

$ go build ./hello.go

コレで 2MB 程度の hello というバイナリファイルがコンパイルされる。

$ ./hello
hello, world!

あとは上のように実行すれば良い。

他にも、.go ファイルをコンパイルせず直接実行する方法もある。import 等の制約があるので、依存関係があるファイルでは上手くいかないこともあるようだが、今回のレベルであれば問題なし。

$ go run ./hello.go
hello, world!

echo コマンド的なモノを作ってみる

続いて、echo コマンドのように、引数を受け取ってコンソール出力する、というプログラムを作ってみる。

package main

import (
  "os"
  "flag"  // コマンドラインオプションのパーサ
)

// -n オプションを用意する。指定した場合は最後に改行を含めないで出力する
var omitNewline = flag.Bool("n", false, "don't print final newline")

// 定数宣言
const (
  Space   = " "
  Newline = "\n"
)

// メイン関数
func main() {
  // パラメータリストを調べて flag に設定する
  flag.Parse()
  
  // 変数宣言
  var s string = ""
  
  // 引数を順に処理する
  for i := 0; i < flag.NArg(); i++ {
    if i > 0 {
      s += Space
    }
    s += flag.Arg(i)
  }
  
  // -n オプションによるフラグがなければ改行を付与する
  if !*omitNewline {
    s += Newline
  }
  
  // コンソール出力する
  os.Stdout.WriteString(s)
}

ココでの留意点は以下のとおり。

  • importvarconst などは、カッコ () でまとめて宣言できる
import (
  "os";    // ← セミコロンはこの位置に打っても、打たなくても良い
  "flag";
)
const (
  Space   = " "
  Newline = "\n"
)

// 以下のように書いたのと同じ
import "os"
import "flag"
const Space   = " "
const Newline = "\n"
  • 変数の型宣言は省略できる
var s string = ""

// コレでも代入している値から string 型と分かる
var s = ""

// var 宣言は以下のように省略して書ける (コード中では for 文で使用している)
s := ""
  • ループは for のみ、while は存在しない
  • iffor の条件部分は、カッコ () で囲まない。ココは Java などの言語と大きく違う点。コードブロックはブレース {} で囲む必要がある
  • if !*omitNewline 部分のアスタリスク * は、ポインタを示すモノらしい。ポインタはサッパリ分からないので要勉強…
  • 何らかの条件によって異常終了させたい場合は、os.Exit(1) という風に書けば良い

このコードを myecho.go として保存したら、以下のように実行できる。

$ go run ./myecho.go HOGE FUGA
HOGE FUGA

ファイルを読み込んでみる

今回の最後は、ファイルを読み込むサンプルコード。ココでかなりつまづいた。

先に動作する正解のコードを載せておく。

  • ./mylib/myfile.go … サブディレクトリ ./mylib/ を作りそこに置く
package myFile  // パッケージ名
import "syscall"

// 型定義
type File struct {
  fd   int     // ファイル記述子番号
  name string  // ファイルを開く時の名前
}

// インスタンスを生成するファクトリ関数
func newFile(fd int, name string) *File {
  if fd < 0 {
    return nil
  }
  return &File{fd, name}
}

// ファイルを開き File 型のインスタンスとして返す
func Open(name string, mode int, perm uint32) (file *File, err error) {
  r, e := syscall.Open(name, mode, perm)
  err = e
  return newFile(r, name), err
}
  • ./read.go … 上の ./mylib/myfile.go を読み込んで利用している
package main
import (
  "./mylib"  // ディレクトリを読み込む
  "fmt"
  "os"
)

func main() {
  // 開きたいファイルのフルパスを指定する
  var path = "/does/not/exist"
  
  // パッケージ名 myFile を指定する
  file, err := myFile.Open(path, 0, 0)
  
  if file == nil {
    fmt.Printf("can't open file; err=%s\n", err)
    os.Exit(1)
  } else {
    fmt.Printf("OK")
  }
}

コードとしてはこんな感じ。

$ go run ./read.go

と実行すると、変数 path で指定したファイルを読み込み、「OK」だったり「can't open file」エラーだったりを出力する、というモノ。

このコードのベースにしたのは、以下の記事。

この記事では、os パッケージを import し、Open() 関数のエラーを os.Error 型で返そうとしていたが、どうも最近の Go 言語では os.Error がなくなっているらしく、このコードはエラーが出て動かなかったので、なんとなくで直した。元はこんなコードが含まれていた。

// 上の動作するコードでは「err = e」とした部分は、代わりにこうなっていた
if e != 0 {
  err = os.Errno(e)
}

err = e と直したら動いたが、コレでいいのかはよく分かっていない。

あと Open() 関数の引数 perm は、int 型では cannot use perm (type int) as type uint32 in argument to syscall.Open とかいうエラーが出てしまったので、unit32 型に変更した。コレもイマイチ分かってない。

大きな変更点はこのくらい。あとは言語仕様について学んだこと。

  • type File struct : type で型定義ができる
  • typeconstfunc などの名前を大文字で始めると、その要素をパッケージ外部に公開できる。コレにより、import した側がその要素を参照できるようになる
    • Node.js でいうと、大文字始まりの定数や関数などが module.exports に自動追加される、みたいな言語仕様になっているようだ。コレは便利かも
    • 例えば func newFile() は小文字の n で始まっているので、read.go から参照することはできない。しかし、func Open() は大文字の O で始まっているのでエクスポートされており、read.gomyFile.Open() と利用できている
  • func newFile() はアンパサンド & やアスタリスク * が登場している。これらはポインタを示す何やららしいが、よく分かっていない。関数の処理的には以下のようにも書けるので、この方が Java の「DTO」っぽい記述の仕方で、慣れてる人の方が多いかも
// newFile() 関数を以下のように書いても同じ
func newFile(fd int, name string) *File {
  var myFile = new(File)
  myFile.fd = fd
  myFile.name = name
  return myFile
}
  • func Open() は、引数指定のあとに (file *File, err error) と書いてある。コレは正常終了時の戻り値と異常終了時の戻り値の2つをセットで宣言しているモノ。Go 言語では戻り値をこのように2つ指定できるようだ
  • func Open() 内の r, e := syscall.Open(name, mode, perm) 部分も同じことで、syscall.Open() 関数が2つの戻り値を返しているので、r (= result)e (= error) の2つを変数で受け取っているのだ
  • 自分が実装した他のファイルをインポートして利用したい場合は、read.go ファイルに書いたように、import "./mylib"ディレクトリパスを指定する
    • こうすると、そのディレクトリ配下にある .go ファイルを読み込めるようになる
  • ディレクトリごと読み込んだ後は、各 .go ファイルの package 宣言で指定した名前を使い、myFile.Open() といった要領で、関数や定数などを参照できる
  • インポートしたファイルたちの中に main() 関数があると、複数の main() 関数が存在するとして実行できないので注意
  • nullnil と表記。Ruby っぽい
  • if 文における else 句は、} else { と1行で書かないとエラーになる。個人的には Java や JS で普段このように1行では書かないので、慣れない…
if file == nil {
  // 処理
}
else {
  // ↑ このように「}」と「else {」を別の行に分けて書くとエラーになる
}

今日はココまで

今回はこの辺りにしておこう。

Go 言語は実行環境のインストールも簡単で、言語仕様も色々と理にかなった作りになっていてとっつきやすそうだ。

ネット上の文献は古今混在しているようで、最後の「ファイル読み込み」の例のように、現在では動かなくなっているコードもあるので、情報の鮮度を見極めて学習していきたい。

スターティングGo言語 (CodeZine BOOKS)

スターティングGo言語 (CodeZine BOOKS)

MacOS で at コマンドを有効化して使ってみる

あるコマンドを予約実行したい場合、すぐ思い付くのは cron (crontab) かと思われる。しかし cron は、ある処理を定期的に繰り返し実行するスケジュール設定になっており、ある処理をある時に1回だけ行いたい場合には使いづらい。

ある処理を1回だけ予約実行したい場合は、at というコマンドを使うと良い。

今回は at コマンドを MacOS Mojave で使ってみる。

at コマンドを有効化する

MacOS で at コマンドを使う際は、どうも最初に at コマンドを有効化する必要があるようだった。

まずは以下のファイルを vi で開く。

$ sudo vi /System/Library/LaunchDaemons/com.apple.atrun.plist

<key>Disabled</key> の次の行に、<true/> と書かれた部分があれば、それを <false/> に書き換える。最初から <false/> であれば問題なし。

続いて以下のコマンドを打つ。

$ sudo launchctl unload -F /System/Library/LaunchDaemons/com.apple.atrun.plist
$ sudo launchctl load -F /System/Library/LaunchDaemons/com.apple.atrun.plist

コレで at コマンドが動作するようになった。

at コマンドを試してみる

at コマンドが動作するか、試しに叩いてみよう。

$ echo 'echo TEST' | at now+10

とコマンドを実行すると、

job 28 at Thu Jun 27 10:49:10 2019

こんな形でジョブがスケジュール登録された反応が返ってくる。この後すぐに atq コマンドを実行してみると、スケジュール登録されている情報が確認できる。

$ atq
28      Thu Jun 27 10:49:00 2019

ジョブは現在時刻から10秒後に実行する設定にしてあるので、余裕をもって2・30秒待ってから再度 atq コマンドを叩くと、今度は何も表示されないと思う。ジョブが実行されてスケジュール一覧から消えたからだ。

実行されたジョブの内容は、MacOS の場合だと、/var/mail/【ユーザ名】 というファイルにログ出力される。正確にはジョブの実行結果をメールする文面が見えているのだが、実行結果ログとして事足りる。

$ tail -f /var/mail/【ユーザ名】

のように叩くと、以下のような内容が表示されるかと思う。

From 【ユーザ名】@【ホスト名】.local  Thu Jun 27 10:52:43 2019
Return-Path: <【ユーザ名】@【ホスト名】.local>
X-Original-To: 【ユーザ名】
Delivered-To: 【ユーザ名】@【ホスト名】.local
Received: by 【ユーザ名】@【ホスト名】.local (Postfix, from userid 501)
        id 1379A9DF000; Thu, 27 Jun 2019 10:52:42 +0900 (JST)
Subject: Output from your job a0001d018d0000
Message-Id: <20190627010000.1379A9DF000@【ホスト名】.local>
Date: Thu, 27 Jun 2019 10:52:42 +0900 (JST)
From: 【ユーザ名】@【ホスト名】.local (Atrun Service)

TEST

こんな感じで実行できていれば OK だ。

at コマンドの叩き方

at コマンドの叩き方はいくつかあるので、代表的なモノを紹介しておこう。

実行日時を指定する

実行日時を指定するには、-t オプションを使うと良い。

$ at -t 1906301720

このように叩くと、「(20) 19年・06月・30日・17時・20分」に実行するよう予約される。

現在時刻より過去の日時を指定した場合は、スケジュール登録されてから順次実行される。at コマンドの発動は若干のタイムラグがあるが、大体1分以内の誤差で動作する感じ。

実行するコマンドを入力する

先程の例では echo 'echo TEST' というコマンドをパイプで at コマンドに渡していたが、コレと同じことは以下のように入力できる。

$ at now+10
# ココで Return キーを押下すると次の行に移動し、コマンドが入力できるようになる
echo TEST
# 実行したいコマンドを入力して Return キーを押下する
# 空行にカーソルが移動したら、Control + D キーを押下する
job 31 at Thu Jun 27 10:58:58 2019
# 以上のようにスケジュール登録できる

キモは Control + D キーだ。コレを知らないと、at コマンドで任意のコマンドを登録する方法が分からないだろう。

コマンドの代わりに実行するファイルを指定する

コマンドを直接入力する代わりに、シェルスクリプトファイルを実行するよう指定できる。

例えば以下のようなシェルスクリプトを用意しておく。

  • ./at-job.sh
$ echo EXECUTED
$ touch OK.txt

そして次のようにスケジュール登録する。

$ at -t 1906301105 -f ./at-job.sh

スケジュールどおりに実行されると、at-job.sh と同じディレクトリに OK.txt というファイルが生成され、/var/mail/【ユーザ名】 ファイルを確認すると、EXECUTED という文言が記録されているだろう。

登録されているジョブを確認する

登録して実行前のジョブを確認するには、先程紹介した atq コマンドか、at -l コマンドを使う。どちらも表示される内容は同じ。

以上

指定の日時に1度だけコマンドを実行する at。MacOS でも簡単に使えたのでオススメ。

Mac Fan Special MacBook完全使いこなしガイド~MacBook・MacBook Air・MacBook Pro/macOS Mojave対応~

Mac Fan Special MacBook完全使いこなしガイド~MacBook・MacBook Air・MacBook Pro/macOS Mojave対応~

Windows10 に「エクスペリエンスインデックス」がないので WinSAT を使う

Windows7 までは、システム情報の画面あたりに「Windows エクスペリエンスインデックス」というスコアデータが表示されていた。CPU・メモリ・GPU・ドライブの性能を数値化したものだ。

どうやらこの機能、Windows8 から廃止されていたらしく、Windows10 でシステム情報画面を開いてもこうしたマシンスペックのスコアデータは確認できなくなっている。

しかし、マシンスペック自体を計測する WinSAT というツールは Windows10 にも内蔵されており、以下の WEI Viewer というサイトに計測結果ファイルをアップロードすれば、かつての「エクスペリエンスインデックス」と同じように可視化できる。

やり方は次のとおり。

  1. PowerShell を起動し、winsat formal コマンドを実行する。コレでベンチマークが開始されるので少し待つ
  2. ベンチマークが完了したら、エクスプローラで C:\Windows\Performance\WinSAT\DataStore\ フォルダを開く
  3. 配下に、最近生成された yyyy-mm-dd hh.mm.ss.xxx Formal.Assessment (Recent).WinSAT.xml といったファイルが存在するかと思う。コレがベンチマーク結果ファイルだ
  4. ブラウザで WEI Viewer サイトを開き、ページ上のどこでも良いので、ベンチマーク結果ファイルをドラッグ・アンド・ドロップする
  5. すると「Windows エクスペリエンスインデックス」と同様の見た目でスコアが表示される

恐らくは Windows7 までの「エクスペリエンスインデックス」も、内部的には winsat formal コマンドの結果を利用していたものと思われる。Windows10 になっても、Windows7 までと変わりなくスコアが確認できたのでよきよき。

Windows 10完全ガイド 基本操作+疑問・困った解決+便利ワザ 改訂2版 (一冊に凝縮)

Windows 10完全ガイド 基本操作+疑問・困った解決+便利ワザ 改訂2版 (一冊に凝縮)