Unverified Commit 1ecdcc2a authored by 老广's avatar 老广 Committed by GitHub

Merge pull request #106 from jumpserver/dev

支持命令过滤
parents 021a70ac d7ba5b58
......@@ -2,11 +2,6 @@
# -*- coding: utf-8 -*-
#
import eventlet
from eventlet.debug import hub_prevent_multiple_readers
eventlet.monkey_patch()
hub_prevent_multiple_readers(False)
import datetime
import os
import time
......@@ -26,7 +21,7 @@ from .session import Session
from .models import Connection
__version__ = '1.4.2'
__version__ = '1.4.3'
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
logger = get_logger(__file__)
......
......@@ -5,6 +5,7 @@
BACKSPACE_CHAR = {b'\x08': b'\x08\x1b[K', b'\x7f': b'\x08\x1b[K'}
ENTER_CHAR = [b'\r', b'\n', b'\r\n']
ENTER_CHAR_ORDER = [ord(b'\r'), ord(b'\n')]
CLEAR_LINE_CHAR = b'\x15'
UNSUPPORTED_CHAR = {b'\x15': 'Ctrl-U', b'\x0c': 'Ctrl-L', b'\x05': 'Ctrl-E'}
CLEAR_CHAR = b'\x1b[H\x1b[2J'
BELL_CHAR = b'\x07'
......
......@@ -2,7 +2,7 @@
#
from functools import wraps
from flask import request, abort
from flask import request, abort, redirect
from ..ctx import app_service
......@@ -23,7 +23,8 @@ def login_required(func):
user = app_service.check_user_cookie(session_id, csrf_token)
request.current_user = user
if not hasattr(request, 'current_user') or not request.current_user:
return abort(403)
url = '/users/login/?next={}'.format(request.path)
return redirect(url)
response = func(*args, **kwargs)
return response
return wrapper
......@@ -50,11 +50,13 @@ def sftp_host_connector_view(host):
@app.route('/coco/elfinder/sftp/<host>/')
@login_required
def sftp_host_finder(host):
return render_template('elfinder/file_manager.html', host=host)
@app.route('/coco/elfinder/sftp/')
@login_required
def sftp_finder():
return render_template('elfinder/file_manager.html', host='_')
......
......@@ -4,7 +4,10 @@ import weakref
import uuid
import socket
from .service import app_service
from .struct import SizedList, SelectEvent
from .utils import wrap_with_line_feed as wr, wrap_with_warning as warning, \
ugettext as _
from . import char
from . import utils
......@@ -140,6 +143,11 @@ class Client:
return "<%s from %s:%s>" % (self.user, self.addr[0], self.addr[1])
class ServerFilter:
def run(self, data):
pass
class BaseServer:
"""
Base Server
......@@ -147,18 +155,23 @@ class BaseServer:
sub-class: Server, Telnet Server
"""
def __init__(self):
self.chan = None
def __init__(self, chan=None):
self.chan = chan
self._session_ref = None
self.input_data = SizedList(maxsize=1024)
self.output_data = SizedList(maxsize=1024)
self._pre_input_state = True
self._in_input_state = True
self._input_initial = False
self._enter_vim_mark = b'\x1b[?25l\x1b[37;1H\x1b[1m'
self._exit_vim_mark = b'\x1b[37;1H\x1b[K\x1b'
self._in_vim_state = False
self.input_data = SizedList(maxsize=1024)
self.output_data = SizedList(maxsize=1024)
self._input = ""
self._output = ""
self._session_ref = None
self._zmodem_recv_start_mark = b'rz waiting to receive.**\x18B0100'
self._zmodem_send_start_mark = b'**\x18B00000000000000'
self._zmodem_cancel_mark = b'\x18\x18\x18\x18\x18'
......@@ -167,6 +180,15 @@ class BaseServer:
self._zmodem_state_recv = 'recv'
self._zmodem_state = ''
self._cmd_parser = utils.TtyIOParser()
self._cmd_filter_rules = self.get_system_user_cmd_filter_rules()
def get_system_user_cmd_filter_rules(self):
rules = app_service.get_system_user_cmd_filter_rules(
self.system_user.id
)
return rules
def set_session(self, session):
self._session_ref = weakref.ref(session)
......@@ -177,51 +199,82 @@ class BaseServer:
else:
return None
def initial_filter(self):
def s_initial_filter(self, data):
if not self._input_initial:
self._input_initial = True
return data
def parse_cmd_filter(self, data):
# 输入了回车键, 开始计算输入的内容
if self._have_enter_char(data):
def s_input_state_filter(self, data):
self._pre_input_state = self._in_input_state
if self._in_vim_state:
self._in_input_state = False
self._input = self._parse_input()
return data
# 用户输入了内容,但是如果没在输入状态,也就是用户刚开始输入了,结算上次输出内容
# 输入了回车键
elif self._have_enter_char(data):
self._in_input_state = False
else:
self._in_input_state = True
return data
def s_parse_input_output_filter(self, data):
# 输入了回车键, 计算输入的命令
if not self._in_input_state:
self._input = self._parse_input()
# 用户输入了内容,但是上次没在输入状态,也就是用户刚开始输入了,结算上次输出内容
if not self._pre_input_state and self._in_input_state:
self._output = self._parse_output()
logger.debug("\n{}\nInput: {}\nOutput: {}\n{}".format(
"#" * 30 + " Command " + "#" * 30,
self._input, self._output,
"#" * 30 + " End " + "#" * 30,
))
# logger.debug("\n{}\nInput: {}\nOutput: {}\n{}".format(
# "#" * 30 + " Command " + "#" * 30,
# self._input, self._output,
# "#" * 30 + " End " + "#" * 30,
# ))
if self._input:
self.session.put_command(self._input, self._output)
self.input_data.clean()
self.output_data.clean()
self._in_input_state = True
return data
def send(self, data):
self.initial_filter()
self.parse_cmd_filter(data)
return self.chan.send(data)
def s_filter_cmd_filter(self, data):
if self._in_input_state:
return data
for rule in self._cmd_filter_rules:
action, cmd = rule.match(self._input)
if action == rule.ALLOW:
break
elif action == rule.DENY:
data = char.CLEAR_LINE_CHAR + b'\r\n'
msg = _("Command `{}` is forbidden ........").format(cmd)
msg = wr(warning(msg.encode()), before=1, after=1)
self.output_data.append(msg)
self.session.send_to_clients(msg)
self.session.put_replay(msg)
break
return data
def replay_filter(self, data):
def r_replay_filter(self, data):
if not self._zmodem_state:
self.session.put_replay(data)
def input_output_filter(self, data):
def r_vim_state_filter(self, data):
if self._zmodem_state:
return data
if self._in_vim_state and data[:11] == self._exit_vim_mark:
self._in_vim_state = False
elif not self._in_vim_state and data[:17] == self._enter_vim_mark:
self._in_vim_state = True
return data
def r_input_output_data_filter(self, data):
if not self._input_initial:
return
return data
if self._zmodem_state:
return
return data
if self._in_input_state:
self.input_data.append(data)
else:
self.output_data.append(data)
return data
def zmodem_state_filter(self, data):
def r_zmodem_state_filter(self, data):
if not self._zmodem_state:
if data[:50].find(self._zmodem_recv_start_mark) != -1:
logger.debug("Zmodem state => recv")
......@@ -237,18 +290,29 @@ class BaseServer:
logger.debug("Zmodem state => cancel")
self._zmodem_state = ''
def zmodem_cancel_filter(self):
def r_zmodem_disable_filter(self, data=''):
if self._zmodem_state:
pass
# self.chan.send(self._zmodem_cancel_mark)
# self.chan.send("Zmodem disabled")
def send(self, data):
self.s_initial_filter(data)
self.s_input_state_filter(data)
try:
self.s_parse_input_output_filter(data)
data = self.s_filter_cmd_filter(data)
except Exception as e:
logger.exception(e)
return self.chan.send(data)
def recv(self, size):
data = self.chan.recv(size)
self.zmodem_state_filter(data)
self.zmodem_cancel_filter()
self.replay_filter(data)
self.input_output_filter(data)
self.r_zmodem_state_filter(data)
self.r_vim_state_filter(data)
self.r_zmodem_disable_filter(data)
self.r_replay_filter(data)
self.r_input_output_data_filter(data)
return data
@staticmethod
......@@ -261,21 +325,19 @@ class BaseServer:
def _parse_output(self):
if not self.output_data:
return ''
parser = utils.TtyIOParser()
return parser.parse_output(self.output_data)
return self._cmd_parser.parse_output(self.output_data)
def _parse_input(self):
if not self.input_data:
return
parser = utils.TtyIOParser()
return parser.parse_input(self.input_data)
return self._cmd_parser.parse_input(self.input_data)
def fileno(self):
return self.chan.fileno()
def close(self):
logger.info("Closed server {}".format(self))
self.input_output_filter(b'')
self.r_input_output_data_filter(b'')
self.chan.close()
def __getattr__(self, item):
......@@ -290,10 +352,9 @@ class TelnetServer(BaseServer):
Telnet server
"""
def __init__(self, sock, asset, system_user):
super(TelnetServer, self).__init__()
self.chan = sock
self.asset = asset
self.system_user = system_user
super(TelnetServer, self).__init__(chan=sock)
class Server(BaseServer):
......@@ -306,11 +367,10 @@ class Server(BaseServer):
# Todo: Server name is not very suitable
def __init__(self, chan, sock, asset, system_user):
super(Server, self).__init__()
self.chan = chan
self.sock = sock
self.asset = asset
self.system_user = system_user
super(Server, self).__init__(chan=chan)
def close(self):
super().close()
......
......@@ -147,6 +147,10 @@ class Session:
pass
self.stop_evt.set()
def send_to_clients(self, data):
for watcher in [self.client] + self._watchers + self._sharers:
watcher.send(data)
def bridge(self):
"""
Bridge clients with server
......
......@@ -90,7 +90,7 @@ class SSHServer:
continue
if not server.event.is_set():
logger.warning("Client not request a valid request, exiting")
logger.warning("Client not request invalid, exiting")
sock.close()
return
else:
......
......@@ -2,6 +2,11 @@
# -*- coding: utf-8 -*-
#
import eventlet
from eventlet.debug import hub_prevent_multiple_readers
eventlet.monkey_patch()
hub_prevent_multiple_readers(False)
import os
import sys
import argparse
......@@ -16,12 +21,9 @@ except ImportError:
print("Please prepare config file `cp conf_example.py conf.py`")
sys.exit(1)
try:
os.mkdir("logs")
os.mkdir("keys")
os.mkdir("sessions")
except:
pass
dirs = ('logs', 'keys')
for d in dirs:
os.makedirs(d, exist_ok=True)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DAEMON = False
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-03 10:39+0800\n"
"POT-Creation-Date: 2018-10-10 15:22+0800\n"
"PO-Revision-Date: 2018-08-10 10:42+0800\n"
"Last-Translator: BaiJiangjie <bugatti_it@163.com>\n"
"Language-Team: Language locale/en/LC\n"
......@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:147
#: coco/app.py:141
msgid "Connect idle more than {} minutes, disconnect"
msgstr ""
......@@ -91,44 +91,44 @@ msgstr ""
msgid "Total: {}"
msgstr ""
#: coco/interactive.py:159
#: coco/interactive.py:161
msgid "There is no matched node, please re-enter"
msgstr ""
#: coco/interactive.py:170
#: coco/interactive.py:172
msgid "ID"
msgstr ""
#: coco/interactive.py:170
#: coco/interactive.py:172
msgid "Hostname"
msgstr ""
#: coco/interactive.py:170
#: coco/interactive.py:172
msgid "IP"
msgstr ""
#: coco/interactive.py:170
#: coco/interactive.py:172
msgid "LoginAs"
msgstr ""
#: coco/interactive.py:184
#: coco/interactive.py:186
msgid "Comment"
msgstr ""
#: coco/interactive.py:192
#: coco/interactive.py:194
msgid "Total: {} Match: {}"
msgstr ""
#: coco/interactive.py:235
#: coco/interactive.py:237
msgid "Select a login:: "
msgstr ""
#: coco/interactive.py:258
#: coco/interactive.py:260
msgid ""
"Terminal does not support login Windows, please use web terminal to access"
msgstr ""
#: coco/interactive.py:269
#: coco/interactive.py:271
msgid "No system user"
msgstr ""
......
# Language locale/en/LC translations for PACKAGE package.
# Copyright (C) 2018 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# BaiJiangjie <bugatti_it@163.com>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-03 10:36+0800\n"
"PO-Revision-Date: 2018-08-10 10:42+0800\n"
"Last-Translator: BaiJiangjie <bugatti_it@163.com>\n"
"Language-Team: Language locale/en/LC\n"
"Language: locale/en/LC_MESSAGES/coco\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:147
msgid "Connect idle more than {} minutes, disconnect"
msgstr ""
#: coco/interactive.py:61
#, python-brace-format
msgid ""
"\n"
"{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system "
"{end}{R}{R}"
msgstr ""
#: coco/interactive.py:63
#, python-brace-format
msgid ""
"{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, "
"Comment{end} to search login(if unique).{R}"
msgstr ""
#: coco/interactive.py:64
#, python-brace-format
msgid ""
"{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} "
"search, such as: /ip.{R}"
msgstr ""
#: coco/interactive.py:65
#, python-brace-format
msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}"
msgstr ""
#: coco/interactive.py:66
#, python-brace-format
msgid ""
"{T}4) Enter {green}g{end} to display the node that you have permission.{R}"
msgstr ""
#: coco/interactive.py:67
#, python-brace-format
msgid ""
"{T}5) Enter {green}g{end} + {green}Group ID{end} to display the host under "
"the node, such as g1.{R}"
msgstr ""
#: coco/interactive.py:68
#, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr ""
#: coco/interactive.py:69
#, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr ""
#: coco/interactive.py:70
#, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr ""
#: coco/interactive.py:142
msgid "No"
msgstr ""
#: coco/interactive.py:149
msgid "Name"
msgstr ""
#: coco/interactive.py:149
msgid "Assets"
msgstr ""
#: coco/interactive.py:155
msgid "Total: {}"
msgstr ""
#: coco/interactive.py:159
msgid "There is no matched node, please re-enter"
msgstr ""
#: coco/interactive.py:170
msgid "ID"
msgstr ""
#: coco/interactive.py:170
msgid "Hostname"
msgstr ""
#: coco/interactive.py:170
msgid "IP"
msgstr ""
#: coco/interactive.py:170
msgid "LoginAs"
msgstr ""
#: coco/interactive.py:184
msgid "Comment"
msgstr ""
#: coco/interactive.py:192
msgid "Total: {} Match: {}"
msgstr ""
#: coco/interactive.py:235
msgid "Select a login:: "
msgstr ""
#: coco/interactive.py:258
msgid ""
"Terminal does not support login Windows, please use web terminal to access"
msgstr ""
#: coco/interactive.py:269
msgid "No system user"
msgstr ""
#: coco/session.py:143
msgid "Terminated by administrator"
msgstr ""
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-03 10:39+0800\n"
"POT-Creation-Date: 2018-10-10 15:22+0800\n"
"PO-Revision-Date: 2018-08-10 10:42+0800\n"
"Last-Translator: BaiJiangjie <bugatti_it@163.com>\n"
"Language-Team: Language locale/zh\n"
......@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:147
#: coco/app.py:141
msgid "Connect idle more than {} minutes, disconnect"
msgstr "空闲时间超过 {} 分钟,断开连接"
......@@ -77,9 +77,9 @@ msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr "{T}7) 输入 {green}h{end} 帮助.{R}"
#: coco/interactive.py:70
#, fuzzy, python-brace-format
#, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr "{T}0) 输入 {green}q{end} 退出.{R}\n"
msgstr "{T}0) 输入 {green}q{end} 退出.{R}"
#: coco/interactive.py:142
msgid "No"
......@@ -97,44 +97,44 @@ msgstr "资产"
msgid "Total: {}"
msgstr "总共: {}"
#: coco/interactive.py:159
#: coco/interactive.py:161
msgid "There is no matched node, please re-enter"
msgstr "没有匹配分组,请重新输入"
#: coco/interactive.py:170
#: coco/interactive.py:172
msgid "ID"
msgstr ""
#: coco/interactive.py:170
#: coco/interactive.py:172
msgid "Hostname"
msgstr "主机名"
#: coco/interactive.py:170
#: coco/interactive.py:172
msgid "IP"
msgstr ""
#: coco/interactive.py:170
#: coco/interactive.py:172
msgid "LoginAs"
msgstr ""
msgstr "登录用户"
#: coco/interactive.py:184
#: coco/interactive.py:186
msgid "Comment"
msgstr "备注"
#: coco/interactive.py:192
#: coco/interactive.py:194
msgid "Total: {} Match: {}"
msgstr "总共: {} 匹配: {}"
#: coco/interactive.py:235
#: coco/interactive.py:237
msgid "Select a login:: "
msgstr "选择一个登录:"
#: coco/interactive.py:258
#: coco/interactive.py:260
msgid ""
"Terminal does not support login Windows, please use web terminal to access"
msgstr "终端不支持登录windows, 请使用web terminal访问"
#: coco/interactive.py:269
#: coco/interactive.py:271
msgid "No system user"
msgstr "没有系统用户"
......
# Language locale/zh translations for PACKAGE package.
# Copyright (C) 2018 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# BaiJiangjie <bugatti_it@163.com>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-03 10:36+0800\n"
"PO-Revision-Date: 2018-08-10 10:42+0800\n"
"Last-Translator: BaiJiangjie <bugatti_it@163.com>\n"
"Language-Team: Language locale/zh\n"
"Language: locale/zh_CN/LC_MESSAGES/coco\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:147
msgid "Connect idle more than {} minutes, disconnect"
msgstr "空闲时间超过 {} 分钟,断开连接"
#: coco/interactive.py:61
#, python-brace-format
msgid ""
"\n"
"{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system "
"{end}{R}{R}"
msgstr ""
"\n"
"{T}{T}{title} {user}, 欢迎使用Jumpserver开源跳板机系统 {end}{R}{R}"
#: coco/interactive.py:63
#, python-brace-format
msgid ""
"{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, "
"Comment{end} to search login(if unique).{R}"
msgstr ""
"{T}1) 输入 {green}ID{end} 直接登录 或 输入{green}部分 IP,主机名,备注{end} 进"
"行搜索登录(如果唯一).{R}"
#: coco/interactive.py:64
#, python-brace-format
msgid ""
"{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} "
"search, such as: /ip.{R}"
msgstr ""
"{T}2) 输入 {green}/{end} + {green}IP, 主机名{end} or {green}备注 {end}搜索. "
"如: /ip{R}"
#: coco/interactive.py:65
#, python-brace-format
msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}"
msgstr "{T}3) 输入 {green}p{end} 显示您有权限的主机.{R}"
#: coco/interactive.py:66
#, python-brace-format
msgid ""
"{T}4) Enter {green}g{end} to display the node that you have permission.{R}"
msgstr "{T}4) 输入 {green}g{end} 显示您有权限的节点.{R}"
#: coco/interactive.py:67
#, python-brace-format
msgid ""
"{T}5) Enter {green}g{end} + {green}Group ID{end} to display the host under "
"the node, such as g1.{R}"
msgstr "{T}5) 输入 {green}g{end} + {green}组ID{end} 显示节点下主机. 如: g1{R}"
#: coco/interactive.py:68
#, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr "{T}6) 输入 {green}s{end} 中/英文切换.{R}"
#: coco/interactive.py:69
#, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr "{T}7) 输入 {green}h{end} 帮助.{R}"
#: coco/interactive.py:70
#, fuzzy, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr "{T}0) 输入 {green}q{end} 退出.{R}\n"
#: coco/interactive.py:142
msgid "No"
msgstr "无"
#: coco/interactive.py:149
msgid "Name"
msgstr "名称"
#: coco/interactive.py:149
msgid "Assets"
msgstr "资产"
#: coco/interactive.py:155
msgid "Total: {}"
msgstr "总共: {}"
#: coco/interactive.py:159
msgid "There is no matched node, please re-enter"
msgstr "没有匹配分组,请重新输入"
#: coco/interactive.py:170
msgid "ID"
msgstr ""
#: coco/interactive.py:170
msgid "Hostname"
msgstr "主机名"
#: coco/interactive.py:170
msgid "IP"
msgstr ""
#: coco/interactive.py:170
msgid "LoginAs"
msgstr ""
#: coco/interactive.py:184
msgid "Comment"
msgstr "备注"
#: coco/interactive.py:192
msgid "Total: {} Match: {}"
msgstr "总共: {} 匹配: {}"
#: coco/interactive.py:235
msgid "Select a login:: "
msgstr "选择一个登录:"
#: coco/interactive.py:258
msgid ""
"Terminal does not support login Windows, please use web terminal to access"
msgstr "终端不支持登录windows, 请使用web terminal访问"
#: coco/interactive.py:269
msgid "No system user"
msgstr "没有系统用户"
#: coco/session.py:143
msgid "Terminated by administrator"
msgstr "被管理员中断"
......@@ -19,7 +19,7 @@ itsdangerous==0.24
Jinja2==2.10
jmespath==0.9.3
jms-storage==0.0.18
jumpserver-python-sdk==0.0.48
jumpserver-python-sdk==0.0.50
MarkupSafe==1.0
oss2==2.4.0
paramiko==2.4.1
......
......@@ -2,15 +2,15 @@
#
function init_message() {
xgettext -k_ -o pot/coco.pot --from-code=UTF-8 coco/*.py
msginit -l locale/zh_CN/LC_MESSAGES/coco -i pot/coco.pot
msginit -l locale/en/LC_MESSAGES/coco -i pot/coco.pot
xgettext -k_ -o /tmp/coco.pot --from-code=UTF-8 coco/*.py
msginit -l locale/zh_CN/LC_MESSAGES/coco -i /tmp/coco.pot
msginit -l locale/en/LC_MESSAGES/coco -i /tmp/coco.pot
}
function make_message() {
xgettext -k_ -o pot/coco.pot --from-code=UTF-8 coco/*.py
msgmerge -U locale/zh_CN/LC_MESSAGES/coco.po pot/coco.pot
msgmerge -U locale/en/LC_MESSAGES/coco.po pot/coco.pot
xgettext -k_ -o /tmp/coco.pot --from-code=UTF-8 coco/*.py
msgmerge -U locale/zh_CN/LC_MESSAGES/coco.po /tmp/coco.pot
msgmerge -U locale/en/LC_MESSAGES/coco.po /tmp/coco.pot
}
function compile_message() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment