Powershellでクリップボードを監視しよう


# Powershellでクリップボードを監視しよう

windows標準環境（Powershell+ブラウザ）でクリップボードを監視しようではないかという投稿です。

## 背景

色々とセキュリティが厳しい昨今、PCにソフト１つ入れるのも難しい環境がお有りではないでしょうか。
クリップボード監視ツールなんてフリーソフトでcliborとかいろいろあるんですが、
そのへんが導入できない壁があり、
windows標準環境だけでクリップボード監視できるものを作ってみようと思い立ちました。

## この記事の対象者

- 会社でWindows使っている方＋なかなかソフトが自由に入れられない方
- Powershellでクリップボード監視する方法に興味がある方

## 今回作るもの

![overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108839/c555cfa6-b751-6dc5-5811-bf0996e45778.png)

## フォルダ構成

```
■フォルダ
│  backup.bat
│  clipboard-monitor.ps1
│  clipboard.html
│  start.bat
│
├─backup
│      yyyymmdd_HHMMSS_clipboard.html
│
└─tmpl
        clipboard.html

```

## ソース一覧

### 主処理起動用のバッチ（start.bat）

powershellを呼び出すバッチを作っておきます

```bat
@echo off
mode 35,10
color 1F
powershell -command "&{$h=Get-Host;$w=$h.UI.RawUI;$s=$w.BufferSize;$s.height=3000;$w.BufferSize=$s;}"
powershell -ExecutionPolicy RemoteSigned .\clipboard-monitor.ps1
```

#### 補足

- 画面幅を縦横指定
- カラーはお好みで...
- 画面幅を指定するとバッファサイズまで画面幅と同じになってしい都合が悪いので、powershellでバッファを3000行取る



### 主処理 監視バッチ（clipboard-monitor.ps1）

次に`start.bat`から呼ばれるpowershellです。
<details><summary>ソースコード</summary><div>

```powershell
# set window title
(Get-Host).UI.RawUI.WindowTitle = $MyInvocation.MyCommand

# get clipboard text
Add-Type -AssemblyName System.Windows.Forms
$clipText = [Windows.Forms.Clipboard]::GetText()
$dateFormat = Get-Date -Format "yyyyMMdd"

#Write-Host $clipText

Write-Host ""
Write-Host " ■■■■■■■■■■■■"
Write-Host " ■"
Write-Host " ■ CLIP BOARD START!"
Write-Host " ■"
Write-Host " ■■■■■■■■■■■■"
Write-Host ""

while ($true) {
  # check the data, whether it do change or not.
  $latestClipText = [Windows.Forms.Clipboard]::GetText()
  if ($latestClipText -ne $clipText) {
    # if changed, update data.
    $clipText = $latestClipText

    # output console.
    Write-Host "- - -"
    Write-Host $clipText
        
    # write data to history file.
    $baseClipText = ${clipText}
    $baseClipText = ${baseClipText} -replace "&", "&amp;"
    $baseClipText = ${baseClipText} -replace "'", "&#x27;"
    $baseClipText = ${baseClipText} -replace "``", "&#x60;"
    $baseClipText = ${baseClipText} -replace '"', "&quot;"
    $baseClipText = ${baseClipText} -replace "<", "&lt;"
    $baseClipText = ${baseClipText} -replace ">", "&gt;"
    $baseClipText = ${baseClipText} -replace "\r\n", "<br/>"
    $baseClipText = ${baseClipText} -replace "\n", "<br/>"
    $time = Get-Date -Format "yyyy/MM/dd HH:mm:ss"
    $fileOutput = "<tr class='cb-tr-clip'><td><span>${time}</span></td><td>${baseClipText}</td></tr>"
    Write-Output ${fileOutput} | Add-Content "clipboard.html" -Encoding UTF8
  }
  # 1秒スリープ
  Start-Sleep -Seconds 1
}

```
</div></details>

#### 補足
- HTMLファイル出力するので、手前でescape処理を挟んでいます。
- １秒おきにクリップボードを監視して、変更があればHTML(clipboard.html)に書き込みに行っています。

### クリップボードテキスト出力先ファイル（clipboard.html）

出力先のHTMLファイルです。
ブラウザで表示した時に確認しやすいようにスタイルをJS処理を書いています。

<details><summary>ソースコード</summary><div>

```html
<head>
  <!-- Required meta tags -->
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

  <!-- Bootstrap CSS -->
  <link
    rel="stylesheet"
    href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
    crossorigin="anonymous"
  />
  <!-- fontawesome -->
  <link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.min.css"
    integrity="sha256-UzFD2WYH2U1dQpKDjjZK72VtPeWP50NoJjd26rnAdUI="
    crossorigin="anonymous"
  />
  <link href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css" rel="stylesheet" />
  <title>Clip4Win</title>
  <style>
    .header-title-text {
      font-size: 5rem;
      letter-spacing: -5px;
      line-height: 140px;
      font-weight: bold;
      color: white;
    }
    .sticky_table thead th {
      /* 縦スクロール時に固定する */
      position: -webkit-sticky;
      position: sticky;
      top: -1px;
      /* tbody内のセルより手前に表示する */
      z-index: 10;
      background-color: black;
    }
    .sticky_table tbody td span {
      /* 縦スクロール時に固定する */
      position: -webkit-sticky;
      position: sticky;
      top: 55px;
      z-index: 9;
    }
    .cb-tr-clip:hover {
      color: #ccc !important;
    }
    .cb-reload {
      position: fixed;
      bottom: 100px;
      right: 100px;
      cursor: pointer;
    }
    input[type="checkbox"]{
      display: none;
    }
    input[type="checkbox"] ~ label {
      border: solid 1px #CCC;
      overflow: hidden;
      width: 50px;
      white-space: nowrap;
    }
    input[type="checkbox"] ~ label::before {
      content: "OFF";
      cursor: pointer;
      width: 50px;
      text-align: center;
      background-color: gray;
      border-right: solid 10px #FFF;
      display: inline-block;
      transition-duration: 0.1s;
      float: left;
    }
    input[type="checkbox"]:checked ~ label::before {
      content: "OFF";
      cursor: pointer;
      display: inline-block;
      transform: translate(-40px, 0px);
    }

    input[type="checkbox"] ~ label::after {
      content: "ON";
      cursor: pointer;
      width: 50px;
      text-align: center;
      background-color: green;
      border-right: solid 10px #FFF;
      display: inline-block;
      transition-duration: 0.1s;
    }
    input[type="checkbox"]:checked ~ label::after {
      content: "ON";
      cursor: pointer;
      background-color: green;
      display: inline-block;
      transform: translate(-40px, 0px);
    }

  </style>
</head>
<body class="container bg-dark text-white" onload="init()">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
  <script
    src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
    integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
    crossorigin="anonymous"
  ></script>
  <script
    src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
    integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
    crossorigin="anonymous"
  ></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
  <script>
    function init() {
      document.querySelectorAll(".cb-tr-clip").forEach(function (el) {
        // console.log("innerHTML > " + el.getElementsByTagName("td")[1].innerHTML);
        // console.log("innerText > " + el.getElementsByTagName("td")[1].innerText);
        // console.log("textContent > " + el.getElementsByTagName("td")[1].textContent);
        //el.getElementsByTagName("td")[1].textContent = escapeHtml(el.getElementsByTagName("td")[1].innerHTML);
        el.getElementsByTagName("td")[1].dataset.text = el.getElementsByTagName("td")[1].innerText;
        el.getElementsByTagName("td")[1].setAttribute("title", el.getElementsByTagName("td")[1].innerText);
        el.getElementsByTagName("td")[1].innerText = tripleReader(el.getElementsByTagName("td")[1].innerText);
        el.addEventListener("click", function () {
          // console.log("click");
          const text = el.getElementsByTagName("td")[1].dataset.text;
          if (execCopy(text)) {
            // alert("Copied!!");
            toastr.options = {
              positionClass: "toast-bottom-right",
              timeOut: "2000",
            };
            toastr.success("Success to Copy");
          } else if (window.getSelection) {
            alert("Failed to copy...");
          }
        });
      });
      document.querySelector("#reload").addEventListener("click", function () {
        window.location.reload();
      });
      var tbody = $("table tbody");
      tbody.html($("tr", tbody).get().reverse());
      
      document.querySelector("#check").checked = localStorage.getItem("isChecked") == "false" ? false : true;
      if (document.querySelector("#check").checked) {
        setInterval(function() {
          window.location.reload();
        }, 10000);
      }
      document.querySelector("#check").addEventListener("change", function() {
        let isChecked = document.querySelector("#check").checked;
        console.log("isChecked:" + isChecked);
        localStorage.setItem("isChecked",isChecked);
        setTimeout(function(){window.location.reload();},1000);
      });
      
//      document.querySelector(".js-change-persec").addEventListener("change", function() {
//        let ps = Number(document.querySelector(".js-change-persec").value);
//        if(!Number(ps)){
//          return;
//        }
//        localstorage.setItem("persec",ps);
//        toastr.success("Success to set Reload Span.");
//      });
    }
    /**
     * コピー処理
     */
    function execCopy(string) {
      // console.log("target string: " + string);
      // 空div 生成
      var tmp = document.createElement("div");
      // 選択用のタグ生成
      var pre = document.createElement("pre");

      // 親要素のCSSで user-select: none だとコピーできないので書き換える
      pre.style.webkitUserSelect = "auto";
      pre.style.userSelect = "auto";

      tmp.appendChild(pre).textContent = string;

      // 要素を画面外へ
      var s = tmp.style;
      s.position = "fixed";
      s.right = "200%";

      // body に追加
      document.body.appendChild(tmp);
      // 要素を選択
      document.getSelection().selectAllChildren(tmp);

      // クリップボードにコピー
      var result = document.execCommand("copy");

      // 要素削除
      document.body.removeChild(tmp);

      return result;
    }
    function tripleReader(text, count) {
      if (!count) count = 30; // default 20
      if (text.length > count) {
        text = text.split("\n")[0].substr(0, count) + "…";
      }
      return text;
    }
    // /**
    //  * HTMLエスケープ処理
    //  */
    // function escapeHtml(string) {
    //   if (typeof string !== "string") {
    //     return string;
    //   }
    //   return string.replace(/[&'`"<>]/g, function (match) {
    //     return {
    //       "&": "&amp;",
    //       "'": "&#x27;",
    //       "`": "&#x60;",
    //       '"': "&quot;",
    //       "<": "&lt;",
    //       ">": "&gt;",
    //     }[match];
    //   });
    // }
  </script>
  <div class="d-flex align-items-center justify-content-center w-100" style="height: 200px;">
    <h1 class="header-title-text">CLIPBOARD</h1>
  </div>
  <div class="cb-reload">
    <div class="d-flex flex-column">
      <!--<div class="my-2">
        <input type="text" class="form-control js-change-persec" style="width:50px;" placeholder="sec">
      </div>-->
      <div>
        <input type="checkbox" id="check" />
        <label for="check" title="Auto Reload (default: per 10seconds)"></label>
      </div>
      <i id="reload" class="fas fa-sync-alt fa-3x"></i>
    </div>
  </div>
  <table class="w-100 table table-hover text-white cb-table-clip sticky_table table-sm">
    <thead class="cb-th-clip">
      <th style="width: 200px;">日付</th>
      <th>コンテンツ</th>
    </thead>
```
</div></details>

### テンプレートファイル（clipboard.html）

./tmpl/clipboard.html に、clipboard.htmlと同じファイルをおいておきます。

### バックアップバッチ（backup.bat）

バックアップを取得するためのバッチです。
出力先の`clipboard.html`が肥大化してきたら実行

<details><summary>ソースコード</summary><div>

```bat
@echo off
set date_str=%date:~-10,4%%date:~-5,2%%date:~-2,2%
rem echo %date_str%@
set time_str=%time: =0%
set time_str=%time_str:~0,2%%time_str:~3,2%%time_str:~6,2%
rem echo %time_str%

echo F | xcopy /Y clipboard.html backup\%date_str%_%time_str%_clipboard.html
xcopy /Y tmpl\clipboard.html clipboard.html
```
</div></details>

### バックアップフォルダ（backup）

- バックアップバッチの出力先として作成しておく！

## 実行方法

上記の`start.bat`を実行するだけでOK！

### 実行イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108839/67d6d148-f0ce-e162-e5e2-9a2dc19f3c91.png)

ブラウザから`clipboard.html`を確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108839/1f4daa4e-0f26-98d4-b406-28fb3ea844dc.png)

- 一覧で履歴を確認できる
- クリックでコピーができる
- マウスホバーでテキスト全体を表示できる

## Github

以下に完成形のソースを上げています。
よろしければご確認ください。

- [画面イメージ](https://tutttuwi.github.io/clip4win/clipboard.html)
- [ソース](https://github.com/tutttuwi/clip4win)

## さいごに

すごく走り書きですが以上です。
ご参考になれば幸いです！