ゆっくり開発

開発したい時に開発するブログ

Microsoft Store からインストールした WSL のための Cmder 設定

Cmder

デフォルトのWSLはタブ機能がなく,また,フォント等の見栄えがよくない. Cmder はそれらを補うためのターミナルエミュレータである. 自分はWSL以外にCmderを使うつもりはないが,PowerShellコマンドプロンプトもエミュレートできる.

Cmder の設定で若干詰まったので,メモ

WSL を Microsoft Store からインストールした場合,Cmder のターミナルセットに Store からインストールした WSL が表示されない.
このため,自分で追加する必要がある.

このセットアップをするに当たって,以下のサイトを参考にした.

laboradian.com

具体的なプリセットの追加方法

  1. Setting を開く
  2. Startup の Tasks を開く
  3. +で Predefined tasks を追加
  4. 左上の枠(Default task for new console の上) にプリセットの名前を入力
  5. 右下の大き目の枠に以下を入力
    %windir%\System32\wsl.exe {xxxxxxxxxxx} ~ -cur_console:p 上記の{xxxxxxxxxxx}は後で編集する.
  6. Win + rで"ファイル名を指定して実行"を開く
  7. regeditを入力し,レジストリエディター開く
  8. HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxssを開き,{xxxxxx}の形式のフォルダをクリックする.(この形式のフォルダは複数ある場合がある.)
  9. DistributionNameの項目を確認することで,どのフォルダが Store からインストールした WSL のものか何となくわかる. Legacyと書いてあったら Store からではないもの.Ubuntu-18.04とか書いてたらきっと Store からインストールしたもの.
  10. Cmder の Setting に戻る.
  11. フォルダ名をコピーし,CmderのSettingの{xxxxxxxxxxx}の部分を書き換える.
  12. Upボタン連打して追加したプリセットを一番上に上げたり,Default task for new console にチェック入れたりする.
  13. StartUp から General に移り,"Choose your startup task or even a shell with arguments"の設定を変更し,追加したプリセットをデフォルトにする.
  14. Save Setting

ドットファイルの install/uninstall スクリプトの作成

クラウドを利用したドットファイルの共有

複数の環境(パソコン)間でドットファイルを共有するために,クラウドを利用すると便利である. このため,最近,ドットファイルの管理にクラウドを導入した.
また,クラウド環境として自分はGitHubを採用した.

ドットファイルの install スクリプトの作成

インストールの手間を短縮

新しい環境を使い始める際や既存の環境をリセットした際の, ドットファイルをインストールする手間を短縮したい. このため,今回はインストールスクリプトを作成する.

インストールスクリプトの設計方針

GitHubで知り合いのリポジトリを漁っていると,以下のコードが釣れた.

#!/bin/bash

for f in .??*
do
    [[ "$f" == ".git" ]] && continue
    [[ "$f" == ".DS_Store" ]] && continue
    ln -s $HOME/dotfiles/$f $HOME/$f
done

上記コードはホームディレクトリにドットファイルのリンクを貼るスクリプトである.このコードを拡張し,インストールスクリプトを作成することにした.

達成したい課題

  1. MacとWSL(Windows)で実行可能
    現状,MacとWSLのターミナルを使っているので, どちらからでも実行できるようにする.
  2. MacとWSLでインストールするドットファイルを変更
    ターミナルが異なる場合,共有したいドットファイルもあれば,共有したくない(異なる環境設定にしたい)ものもある. このため,異なるターミナル,今回はMacとWSLでインストールするドットファイルを変更する.
  3. リンクが貼れない状況に対応
    貼りたいドットファイルと同名のファイルが既にホームディレクトリに存在する場合,$ lnコマンドにエラーを出力させず,自分が設定した文字列を出力する(エラー処理).

各課題への対処

MacとWSL(Windows)で実行可能

Macで実行できたコードがWSLで動かない,といったことは何度もあったが,正直,原因がよくわからなかった. 色々試した結果,以下の3点を行うことでうまく動いた.

  • functionを使わない.
    WSLの方でエラーを吐かれた.このため,functionを使わず,代わりに別ファイルを作成し,そのファイル(スクリプト)を呼び出すことにした.
  • bashではなく,shで実行する.
    shebangbashからshに変えるだけでなく,以下の様に実行コマンドもshを使う.
    $ sh ./install.sh
  • 条件式に[[を使わず,全て[を使う.
    [[bashのコマンドらしいので,[に変更する.
MacとWSLでインストールするドットファイルを変更

以下の方法で対処する.

以下のコードで実行OS(ターミナル)を判断し,ホームディレクトリに共有ドットファイルと専用ドットファイルのリンクを貼る.

#!/bin/sh

# OS(ターミナル)の種類を判断
if [ "$(uname)" = 'Darwin' ]; then
  OS='Mac'
elif [ $(echo $(uname -r) | grep -e 'Microsoft') ]; then
  OS='WSL'
else
  echo "Your platform ($(uname -a)) is not supported."
  OS='undefined'
fi
echo $OS' terminal\n'

# 各OS(ターミナル)に応じて dotfile のリンクを貼る.
if [ ! $OS = "undefined" ]; then
  sh scripts/link_dotfiles_to_home.sh $OS"/"
fi

# 各OS(ターミナル)共通の dotfile のリンクを貼る.
sh scripts/link_dotfiles_to_home.sh ""

また,link_dotfiles_to_home.shは指定したディレクトリ下のドットファイルのリンクをホームディレクトリに貼るスクリプトである. 第一引数でそのディレクトリを指定する.
これは参考コードのfor f in .??*for f in $1.??*に改変することで実現した.

リンクが貼れない状況に対応

以下の状況を条件文により分岐し,処理.

  • 既にファイルがある場合
    [NG] を出力
  • 既にリンクがある場合
    [skip] を出力
  • それ以外(リンクが貼れる場合)
    リンクを貼り,[OK] を出力

また,各[NG], [skip] および [OK] のecho出力には色を付けた.
link_dotfiles_to_home.shソースコードは以下の通り.

#!/bin/sh

for f in $1.??*
do
  # ファイルの絶対パスを取得
  dotfile_path=$HOME"/dotfiles/"$f
  # ファイルパスの一番右の"/"以降の文字列を取得
  dotfile_name=${dotfile_path##*/}

  # 特定のファイルは pass
  [ "$dotfile_name" = ".git" ] && continue
  [ "$dotfile_name" = ".gitignore" ] && continue
  [ "$dotfile_name" = ".DS_Store" ] && continue

  # ファイルのリンクが既に貼られていれば skip を出力
  if [ -h $HOME"/"$dotfile_name ]; then
    echo "[\033[36mskip\033[0m] "$dotfile_name
  # ファイルの実体(!=リンク)があれば,NG を出力
  elif [ -e $HOME"/"$dotfile_name ]; then
    echo "[\033[31mNG\033[0m] "$dotfile_name
  # ファイルのリンクが貼られていなければ,シンボリックリンクを貼る.
  else
    ln -s $dotfile_path $HOME"/"$dotfile_name
    echo "[\033[32mOK\033[0m] "$dotfile_name
  fi
done

install スクリプトの実行による出力は以下の通り.

f:id:uttnaoki:20180627235523p:plain:w200f:id:uttnaoki:20180627235529p:plain:w200
dotfiles_install_fig

uninstall スクリプトの作成

処理内容はほぼ install スクリプトと同じで,作成されたリンクをアンリンクするだけ. 具体的に,link_dotfiles_to_home.shのソースの一部を以下のコードに改変し,delete_symbolic_links.shとして保存する. uninstall.shinstall.shlink_dotfiles_to_home.shを呼ぶコードをdelete_symbolic_links.shを呼ぶコードに変えるだけ.

#!/bin/sh

for f in $1.??*
do
  # ファイルの絶対パスを取得
  dotfile_path=$HOME/dotfiles/$f
  # ファイルパスの一番右の"/"以降の文字列を取得
  dotfile_name=${dotfile_path##*/}

  # 特定のファイルは pass
  [ "$dotfile_name" = ".git" ] && continue
  [ "$dotfile_name" = ".gitignore" ] && continue
  [ "$dotfile_name" = ".DS_Store" ] && continue

  # ファイルのリンクが既に貼られていれば,ホームディレクトリのリンクを削除
  if [ -h $HOME"/"$dotfile_name ]; then
    unlink $HOME"/"$dotfile_name
    echo "[\033[32mOK\033[0m] "$dotfile_name
  # ファイルの実体(!=リンク)があれば,NG を出力
  elif [ -e $HOME"/"$dotfile_name ]; then
    echo "[\033[31mNG\033[0m] "$dotfile_name
  # ファイルのリンクが貼られていなければ skip
  else
    echo "[\033[36mskip\033[0m] "$dotfile_name
  fi
done

CSVファイルの編集(データ整形)の効率化

Excelを使ったデータ整形から卒業したい

現状,手軽なCSVデータの整形方法は以下の2パターン

  1. Excelの関数やオートフィルを使った整形
  2. テキストエディタの置換等を利用した整形

上記方法でも短時間で柔軟なデータ整形が可能ではあるが, せっかく大学や趣味でプログラミングの勉強をしているので, python等のスクリプトコードを利用してデータ整形したい.

しかし,データを整形する度にプログラムを一から作ることは面倒である. また,アルゴリズムを考え,細かい文法を思い出す(調べる)必要がある場合もある. これらの対処として,今回は以下を行う.

  1. データ整形用コードのテンプレートを作成
  2. 整形するデータに応じたスクリプトを,どのディレクトリからでもコピーできる環境を構築

データ整形用コードのテンプレート

今回は「特定の列について,元の値に応じた編集」を行うためのpythonスクリプトを作成する. 例えば,以下のような編集を想定する.

  • ある列の内容を,各値に応じて「大」「中」「小」といったカテゴリに書き換える.
  • 単純に,特定の列の各値(セル)の先頭に特定の文字列を追加する.

上記を考慮したデータ整形用コード(python3)は以下の通り.

import pandas as pd
imput_file = 'input.csv'
output_file = 'output.csv'

csv = pd.read_csv(imput_file)
header = csv.columns
colnum = len(header)

output_text = "{0}\n".format(', '.join(header))
for index, row in csv.iterrows():
    # row['列名']の値を編集することで,ファイルを編集できます.

    output_row = [row['大会名'], row['1位'], row['2位'], row['3位']]
    output_row_str = map(str, output_row)
    output_row = ', '.join(output_row_str)

    # 改行込みで変数に格納
    output_text += output_row + '\n'

# 編集した内容をファイルに出力
f = open(output_file, 'w')
f.write(output_text)
f.close()

上記コードの中で,row['列名']変数の値を書き換えることで,出力ファイルの内容が変わる.

対象データに応じてテンプレートの内容を変更

テンプレートコードの以下の2行について,

imput_file = 'input.csv'
output_row = [row['大会名'], row['1位'], row['2位'], row['3位']]

この2行を対象データに応じて変更する.
具体的に,input_file変数には文字列は対象データのファイル名を代入し, output_row変数に代入する配列は対象データのヘッダー名に対応する.

これを,以下の手順により実現する.

  1. テンプレートコードの上記2行を適当な文字列に書き換えたファイル(editCSV.template.py)を用意
  2. 別のpythonコード(genEditCSV.py)で対象データを読み込む
  3. 対象データのファイル名とヘッダー名に応じたデータ整形用コード(editCSV.py)を出力

editCSV.template.pyについて

editCSV.pyとの差分は(+/-)で示す.

  import pandas as pd
- imput_file = 'input.csv'
+ imput_file = # {{input_file}}
  output_file = 'output.csv'

  csv = pd.read_csv(imput_file)
  header = csv.columns
  colnum = len(header)

  output_text = "{0}\n".format(', '.join(header))
  for index, row in csv.iterrows():
      # row['列名']の値を編集することで,ファイルを編集できます.

-     output_row = [row['大会名'], row['1位'], row['2位'], row['3位']]
+     output_row = # {{output_row}}
      output_row_str = map(str, output_row)
      output_row = ', '.join(output_row_str)

      # 改行込みで変数に格納
      output_text += output_row + '\n'

  # 編集した内容をファイルに出力
  f = open(output_file, 'w')
  f.write(output_text)
  f.close()

genEditCSV.pyについて

import csv
import sys
import os

args = sys.argv

# 引数のCSVファイル名について,validation
if len(args) < 2:
    print('CSVファイルを引数で指定してください.')
    sys.exit(1)
if not os.path.isfile(args[1]):
    print('CSVファイル {0} がありません.'.format(input_file))
    sys.exit(1)

input_file = args[1]
output_file = 'editCSV.py'
this_path = '{0}/scripts/editCSV'.format(os.environ['HOME'])

f = open(input_file, 'r')
reader = csv.reader(f)
header = next(reader)
colnum = len(header)

# editCSV.py に出力するコードを作成
output_code = ''
for i in range(colnum):
    output_code += "row['{0}'], ".format(header[i])
f.close()

output_code = "output_row = [{0}]".format(output_code[:-2])

# editCSV.template.py の '# {{output}}' の部分に output_code を置換
f_template = open('{0}/editCSV.template.py'.format(this_path), 'r')
f_output = open(output_file, 'w')
for row in f_template:
    # '# {{input_file}}' の行を探し,置換
    if row.find('# {{input_file}}'):
        row = row.replace('# {{input_file}}', "'{0}'".format(input_file))
    # '# {{output_row}}' の行を探し,置換
    if row.find('# {{output_row}}'):
        row = row.replace('# {{output_row}}', output_code)
    f_output.write(row)
f_output.close()
f_template.close()

任意のディレクトリにeditCSV.pyを出力

自分の環境ではホームディレクトリ直下にscriptsディレクトリを配置しており, このディレクトリ下に以下のファイルを配置した.

  • scripts/
    • editCSV/
      • editCSV.template.py
      • genEditCSV.py

このgenEditCSV.pyを任意のディレクトリから呼び出せるように,以下の様なaliasを貼った.
$ alias editCSV='python3 ~/scripts/editCSV/genEditCSV.py'

ここまでで,今回の開発は終了.

未対応の課題について

  • 特定の項目名が空であるデータや,そもそもヘッダーがないデータへの対応
  • 列の追加・削除
  • windowsで実行した場合,pandasの処理により数値が少数(10 -> 10.0)に変換される.